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,148 @@
|
||||
package address
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseHouseBlock(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
raw string
|
||||
wantOrdinal string
|
||||
wantName string
|
||||
wantType string
|
||||
wantHouse string
|
||||
wantFrac string
|
||||
wantLetter string
|
||||
wantBuild string
|
||||
wantBFrac string
|
||||
wantBLetter string
|
||||
wantStruct string
|
||||
wantHas bool
|
||||
}{
|
||||
{
|
||||
name: "ordinal, fraction and structure",
|
||||
raw: "2-я Станционная улица 1/1 стр.",
|
||||
wantOrdinal: "2", wantName: "станционная", wantType: "улица",
|
||||
wantHouse: "1", wantFrac: "1", wantStruct: "", wantHas: true,
|
||||
},
|
||||
{
|
||||
name: "building with fraction and structure",
|
||||
raw: "Станционная улица 30а к9/1 стр.",
|
||||
wantName: "станционная", wantType: "улица",
|
||||
wantHouse: "30", wantLetter: "а", wantBuild: "9", wantBFrac: "1", wantHas: true,
|
||||
},
|
||||
{
|
||||
name: "letter glued to house",
|
||||
raw: "10-й Бронный переулок 33а",
|
||||
wantOrdinal: "10", wantName: "бронный", wantType: "переулок",
|
||||
wantHouse: "33", wantLetter: "а", wantHas: true,
|
||||
},
|
||||
{
|
||||
name: "microdistrict, two-word name",
|
||||
raw: "Зеленый бор микрорайон 13",
|
||||
wantName: "бор зеленый", wantType: "микрорайон",
|
||||
wantHouse: "13", wantHas: true,
|
||||
},
|
||||
{
|
||||
name: "spelled-out ordinal",
|
||||
raw: "вторая Станционная улица 1",
|
||||
wantOrdinal: "2", wantName: "станционная", wantType: "улица",
|
||||
wantHouse: "1", wantHas: true,
|
||||
},
|
||||
{
|
||||
name: "en dash variant",
|
||||
raw: "2–я Станционная улица 1",
|
||||
wantOrdinal: "2", wantName: "станционная", wantType: "улица",
|
||||
wantHouse: "1", wantHas: true,
|
||||
},
|
||||
{
|
||||
name: "no house",
|
||||
raw: "Станционная улица",
|
||||
wantName: "станционная", wantType: "улица", wantHas: false,
|
||||
},
|
||||
{
|
||||
name: "structure with number only",
|
||||
raw: "Станционная улица 3 стр.",
|
||||
wantName: "станционная", wantType: "улица",
|
||||
wantHouse: "3", wantStruct: "", wantHas: true,
|
||||
},
|
||||
{
|
||||
name: "letter before fraction",
|
||||
raw: "Выборная улица 45а/4",
|
||||
wantName: "выборная", wantType: "улица",
|
||||
wantHouse: "45", wantLetter: "а", wantFrac: "4", wantHas: true,
|
||||
},
|
||||
{
|
||||
name: "corpus letter without digit",
|
||||
raw: "1-я Ельцовка улица 1 кД",
|
||||
wantOrdinal: "1", wantName: "ельцовка", wantType: "улица",
|
||||
wantHouse: "1", wantBLetter: "д", wantHas: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Parse(tt.raw)
|
||||
if got.Ordinal != tt.wantOrdinal {
|
||||
t.Errorf("Ordinal = %q, want %q", got.Ordinal, tt.wantOrdinal)
|
||||
}
|
||||
if got.NameKey != tt.wantName {
|
||||
t.Errorf("NameKey = %q, want %q", got.NameKey, tt.wantName)
|
||||
}
|
||||
if got.StreetType != tt.wantType {
|
||||
t.Errorf("StreetType = %q, want %q", got.StreetType, tt.wantType)
|
||||
}
|
||||
if got.House != tt.wantHouse {
|
||||
t.Errorf("House = %q, want %q", got.House, tt.wantHouse)
|
||||
}
|
||||
if got.HouseFrac != tt.wantFrac {
|
||||
t.Errorf("HouseFrac = %q, want %q", got.HouseFrac, tt.wantFrac)
|
||||
}
|
||||
if got.HouseLetter != tt.wantLetter {
|
||||
t.Errorf("HouseLetter = %q, want %q", got.HouseLetter, tt.wantLetter)
|
||||
}
|
||||
if got.Building != tt.wantBuild {
|
||||
t.Errorf("Building = %q, want %q", got.Building, tt.wantBuild)
|
||||
}
|
||||
if got.BuildingFrac != tt.wantBFrac {
|
||||
t.Errorf("BuildingFrac = %q, want %q", got.BuildingFrac, tt.wantBFrac)
|
||||
}
|
||||
if got.BuildingLetter != tt.wantBLetter {
|
||||
t.Errorf("BuildingLetter = %q, want %q", got.BuildingLetter, tt.wantBLetter)
|
||||
}
|
||||
if got.Structure != tt.wantStruct {
|
||||
t.Errorf("Structure = %q, want %q", got.Structure, tt.wantStruct)
|
||||
}
|
||||
if got.HasHouse != tt.wantHas {
|
||||
t.Errorf("HasHouse = %v, want %v", got.HasHouse, tt.wantHas)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseDeterministic guards the core invariant: parsing the same input
|
||||
// always yields the same structured result and the same street key.
|
||||
func TestParseDeterministic(t *testing.T) {
|
||||
input := "2-я Станционная улица 1/1 стр."
|
||||
first := Parse(input)
|
||||
for i := 0; i < 100; i++ {
|
||||
if got := Parse(input); got.StreetKey != first.StreetKey || got.House != first.House {
|
||||
t.Fatalf("non-deterministic parse: %+v vs %+v", got, first)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeIdempotent(t *testing.T) {
|
||||
inputs := []string{
|
||||
" 2-Я Станционная\u00a0улица 1/1 стр. ",
|
||||
"ЁЛКИ д. 5, кв. 3",
|
||||
"СТАНИЦИОННАЯ — улица, 30а",
|
||||
"",
|
||||
}
|
||||
for _, in := range inputs {
|
||||
once := NormalizeText(in)
|
||||
twice := NormalizeText(once)
|
||||
if once != twice {
|
||||
t.Errorf("NormalizeText not idempotent for %q: %q != %q", in, once, twice)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user