addrkit: strict ordinal matching, LLM-variant guard, externalized prompt
- match: require candidate ordinal to equal the query ordinal in fuzzy search (deterministic typo recall 73% -> 80%, no wrong houses) - resolver: compatibleVariant guard — LLM may only fix street spelling, keeping the same words, ordinal and house unit; damerau distance for transpositions - llm: prompt moved out of code (go:embed default_prompt.txt) with llm.system_prompt / llm.system_prompt_file overrides; guided JSON parsing tolerant of thinking blocks - address: recognize ordinals appearing after the street name - tests: first coverage for the LLM adapter and config prompt loading; README Measured (sample 200): deterministic 94.7-95.8% exact / 0 wrong across 4 seeds; LLM+guard typo 90.7-89.8% / 0 wrong. LLM fallback latency ~1.2-1.9s.
This commit is contained in:
@@ -120,13 +120,16 @@ func parseStreet(p *ParsedAddress, s string) {
|
||||
p.StreetKey = streetKey(p.Ordinal, p.NameKey)
|
||||
}
|
||||
|
||||
// extractOrdinal recognises a leading ordinal ("2-я", "2 я", "вторая") and
|
||||
// removes it from the name tokens.
|
||||
// extractOrdinal recognises an ordinal and removes it from the name tokens.
|
||||
// It handles both the usual leading form ("2-я Сухарная") and an ordinal that
|
||||
// appears after the name ("Сухарная 2-я улица"), which both people and models
|
||||
// produce.
|
||||
func extractOrdinal(p *ParsedAddress, tokens []string) []string {
|
||||
if len(tokens) == 0 {
|
||||
return tokens
|
||||
}
|
||||
|
||||
// Leading forms first: they are by far the most common.
|
||||
if m := reOrdinalToken.FindStringSubmatch(tokens[0]); m != nil {
|
||||
p.Ordinal = normalizeOrdinal(m[1])
|
||||
return tokens[1:]
|
||||
@@ -139,9 +142,29 @@ func extractOrdinal(p *ParsedAddress, tokens []string) []string {
|
||||
p.Ordinal = normalizeOrdinal(d)
|
||||
return tokens[1:]
|
||||
}
|
||||
|
||||
// Otherwise look for an ordinal anywhere among the name tokens.
|
||||
for i, tok := range tokens {
|
||||
if m := reOrdinalToken.FindStringSubmatch(tok); m != nil {
|
||||
p.Ordinal = normalizeOrdinal(m[1])
|
||||
return removeRange(tokens, i, i+1)
|
||||
}
|
||||
if isDigits(tok) && i+1 < len(tokens) && ordinalSuffixes[tokens[i+1]] {
|
||||
p.Ordinal = normalizeOrdinal(tok)
|
||||
return removeRange(tokens, i, i+2)
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
// removeRange returns a copy of tokens with [from, to) removed.
|
||||
func removeRange(tokens []string, from, to int) []string {
|
||||
out := make([]string, 0, len(tokens)-(to-from))
|
||||
out = append(out, tokens[:from]...)
|
||||
out = append(out, tokens[to:]...)
|
||||
return out
|
||||
}
|
||||
|
||||
// matchSuffix returns the capture groups of the match of re that ends exactly
|
||||
// at the end of s, together with the byte offset where it starts. Go's regexp
|
||||
// is leftmost-first, so we scan all matches and pick the right-anchored one.
|
||||
|
||||
Reference in New Issue
Block a user