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:
addrkit
2026-10-09 20:40:20 +07:00
parent 5cc3556ac5
commit 78691f3a22
17 changed files with 590 additions and 48 deletions
+39
View File
@@ -52,6 +52,45 @@ func ratio(a, b string) float64 {
return 1 - float64(levenshtein(a, b))/float64(m)
}
// damerau returns the optimal string alignment (Damerau-Levenshtein) distance,
// where an adjacent transposition costs 1. Typos are very often transpositions
// ("Гоглоя" vs "Гоголя"), so counting them as a single edit matters.
func damerau(a, b string) int {
ra, rb := []rune(a), []rune(b)
la, lb := len(ra), len(rb)
if la == 0 {
return lb
}
if lb == 0 {
return la
}
// Three rolling rows: d[i-2], d[i-1], d[i].
prev2 := make([]int, lb+1)
prev := make([]int, lb+1)
curr := make([]int, lb+1)
for j := 0; j <= lb; j++ {
prev[j] = j
}
for i := 1; i <= la; i++ {
curr[0] = i
for j := 1; j <= lb; j++ {
cost := 1
if ra[i-1] == rb[j-1] {
cost = 0
}
best := min3(prev[j]+1, curr[j-1]+1, prev[j-1]+cost)
if i > 1 && j > 1 && ra[i-1] == rb[j-2] && ra[i-2] == rb[j-1] {
if t := prev2[j-2] + 1; t < best {
best = t
}
}
curr[j] = best
}
prev2, prev, curr = prev, curr, prev2
}
return prev[lb]
}
func min3(a, b, c int) int {
m := a
if b < m {