addrkit: deterministic address resolution service (Go)
- domain: address normalization/parsing, matching with explicit statuses - adapters: SQLite registry store, ArcGIS outages client, optional LLM rewriter, HTTP API - seed pipeline with duplicate handling and unparsed reporting - CLI (seed/verify/resolve), systemd unit, OpenAPI contract, tests
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
package match
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.totmin.ru/en2zmax/addrkit/internal/domain/address"
|
||||
)
|
||||
|
||||
// buildIndex parses canonical addresses into entries, mirroring what the seed
|
||||
// pipeline does, so tests exercise the real code path end to end.
|
||||
func buildIndex(t *testing.T, addresses ...string) *Index {
|
||||
t.Helper()
|
||||
entries := make([]address.Entry, 0, len(addresses))
|
||||
for i, a := range addresses {
|
||||
p := address.Parse(a)
|
||||
entries = append(entries, address.Entry{
|
||||
ID: a, Address: a, District: "Тестовый",
|
||||
Ordinal: p.Ordinal, NameKey: p.NameKey, StreetType: p.StreetType, StreetKey: p.StreetKey,
|
||||
House: p.House, HouseFrac: p.HouseFrac, HouseLetter: p.HouseLetter,
|
||||
Building: p.Building, BuildingFrac: p.BuildingFrac, BuildingLetter: p.BuildingLetter,
|
||||
Structure: p.Structure,
|
||||
})
|
||||
_ = i
|
||||
}
|
||||
return NewIndex("test", entries, DefaultOptions())
|
||||
}
|
||||
|
||||
func TestResolveStatuses(t *testing.T) {
|
||||
ix := buildIndex(t,
|
||||
"Станционная улица 1/1",
|
||||
"Станционная улица 2/1",
|
||||
"Станционная улица 3/1 к1",
|
||||
"Станционная улица 3/1 к2",
|
||||
"Искитимская улица 5",
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
wantStatus address.Status
|
||||
wantAddr string
|
||||
}{
|
||||
{
|
||||
name: "found with ordinal, fraction and structure noise",
|
||||
query: "2-я Станционная улица 1/1 стр.",
|
||||
wantStatus: address.StatusFound,
|
||||
wantAddr: "Станционная улица 1/1",
|
||||
},
|
||||
{
|
||||
name: "found exact house",
|
||||
query: "станционная 2/1",
|
||||
wantStatus: address.StatusFound,
|
||||
wantAddr: "Станционная улица 2/1",
|
||||
},
|
||||
{
|
||||
name: "found with corpus",
|
||||
query: "Станционная улица 3/1 корпус 1",
|
||||
wantStatus: address.StatusFound,
|
||||
wantAddr: "Станционная улица 3/1 к1",
|
||||
},
|
||||
{
|
||||
name: "typo in street name tolerated",
|
||||
query: "станционая 1/1",
|
||||
wantStatus: address.StatusFound,
|
||||
wantAddr: "Станционная улица 1/1",
|
||||
},
|
||||
{
|
||||
name: "missing house is partial",
|
||||
query: "станционная улица",
|
||||
wantStatus: address.StatusPartial,
|
||||
},
|
||||
{
|
||||
name: "wrong house fraction is partial",
|
||||
query: "станционная 1/2",
|
||||
wantStatus: address.StatusPartial,
|
||||
},
|
||||
{
|
||||
name: "ambiguous building",
|
||||
query: "станционная 3/1",
|
||||
wantStatus: address.StatusAmbiguous,
|
||||
},
|
||||
{
|
||||
name: "unknown street",
|
||||
query: "ленина 5",
|
||||
wantStatus: address.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
res := ix.Resolve(address.Parse(tt.query))
|
||||
if res.Status != tt.wantStatus {
|
||||
t.Fatalf("status = %q, want %q (candidates: %+v)", res.Status, tt.wantStatus, res.Candidates)
|
||||
}
|
||||
if tt.wantAddr != "" && res.Address != tt.wantAddr {
|
||||
t.Errorf("address = %q, want %q", res.Address, tt.wantAddr)
|
||||
}
|
||||
if res.DataVersion != "test" {
|
||||
t.Errorf("data version = %q, want test", res.DataVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveNeverInventsAddress is the central safety property: whenever the
|
||||
// status is "found", the returned address must exist in the index.
|
||||
func TestResolveNeverInventsAddress(t *testing.T) {
|
||||
addresses := []string{
|
||||
"Станционная улица 1/1",
|
||||
"Станционная улица 2/1",
|
||||
"Искитимская улица 5",
|
||||
}
|
||||
ix := buildIndex(t, addresses...)
|
||||
known := make(map[string]bool, len(addresses))
|
||||
for _, a := range addresses {
|
||||
known[a] = true
|
||||
}
|
||||
queries := []string{
|
||||
"2-я Станционная 1/1", "станционная 2/1 к9", "искитимская 5", "искитимская 5/1",
|
||||
"несуществующая 99", "станционная 1/1 стр.3", " ",
|
||||
}
|
||||
for _, q := range queries {
|
||||
res := ix.Resolve(address.Parse(q))
|
||||
if res.Status == address.StatusFound && !known[res.Address] {
|
||||
t.Fatalf("invented address %q for query %q", res.Address, q)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRatio(t *testing.T) {
|
||||
if got := ratio("станционная", "станционная"); got != 1 {
|
||||
t.Errorf("identical strings ratio = %v, want 1", got)
|
||||
}
|
||||
if got := ratio("станционная", "искитимская"); got > 0.6 {
|
||||
t.Errorf("unrelated strings ratio = %v, want < 0.6", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user