addrkit: 'selfcheck' command — resolve the whole registry against itself

Exhaustive check of all 72,825 indexed addresses: 72,811 exact (99.98%),
14 partial (malformed house units, e.g. '27с1', '32 к 1/4-1/6'), 0 not_found,
0 ambiguous.
This commit is contained in:
Maksim Totmin
2026-10-09 21:40:46 +07:00
parent f1537f1d6b
commit 58272c5f9d
+58
View File
@@ -16,6 +16,7 @@ import (
"git.totmin.ru/en2zmax/addrkit/internal/adapters/llm"
"git.totmin.ru/en2zmax/addrkit/internal/adapters/registrystore"
"git.totmin.ru/en2zmax/addrkit/internal/config"
"git.totmin.ru/en2zmax/addrkit/internal/domain/address"
"git.totmin.ru/en2zmax/addrkit/internal/domain/match"
"git.totmin.ru/en2zmax/addrkit/internal/eval"
"git.totmin.ru/en2zmax/addrkit/internal/seed"
@@ -35,6 +36,8 @@ func main() {
os.Exit(runResolve(os.Args[2:]))
case "eval":
os.Exit(runEval(os.Args[2:]))
case "selfcheck":
os.Exit(runSelfcheck(os.Args[2:]))
case "help", "-h", "--help":
usage()
os.Exit(0)
@@ -53,6 +56,7 @@ Usage:
addrkit verify -db registry.db
addrkit resolve -db registry.db "2-я Станционная улица 1/1 стр."
addrkit eval -db registry.db [-sample 500] [-seed 1]
addrkit selfcheck -db registry.db
`)
}
@@ -265,3 +269,57 @@ func (c *countingRewriter) avg() time.Duration {
}
return c.total / time.Duration(c.calls)
}
// runSelfcheck resolves every indexed address against the index and reports how
// many resolve back to themselves. This is the strongest answer to "do all
// addresses resolve?": it exercises the whole registry, not a sample.
func runSelfcheck(args []string) int {
fs := flag.NewFlagSet("selfcheck", flag.ExitOnError)
db := fs.String("db", "", "SQLite database")
_ = fs.Parse(args)
if *db == "" {
fmt.Fprintln(os.Stderr, "selfcheck: -db is required")
return 2
}
_, entries, err := registrystore.Load(*db)
if err != nil {
fmt.Fprintf(os.Stderr, "selfcheck: %v\n", err)
return 1
}
resolver := match.NewResolver(match.NewIndex("selfcheck", entries, match.DefaultOptions()), nil)
exact := 0
byStatus := map[string]int{}
var failures []string
for _, e := range entries {
res := resolver.Resolve(context.Background(), e.Address)
if res.Status == address.StatusFound && sameEntry(res.Address, e) {
exact++
continue
}
byStatus[string(res.Status)]++
if len(failures) < 40 {
failures = append(failures, fmt.Sprintf("[%s] %q -> %q", res.Status, e.Address, res.Address))
}
}
out := map[string]any{
"entries": len(entries),
"exact": exact,
"exact_rate": float64(exact) / float64(max(len(entries), 1)),
"non_exact": byStatus,
"failures": failures,
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
_ = enc.Encode(out)
return 0
}
// sameEntry reports whether a resolved address is the same street and house
// unit as the registry entry it came from.
func sameEntry(resolved string, e address.Entry) bool {
p := address.Parse(resolved)
return p.StreetKey == e.StreetKey &&
p.House == e.House && p.HouseFrac == e.HouseFrac && p.HouseLetter == e.HouseLetter &&
p.Building == e.Building && p.BuildingFrac == e.BuildingFrac && p.BuildingLetter == e.BuildingLetter
}