Files
addrkit/internal/domain/address/parse_test.go
T
Maksim Totmin ba4705fc4f addrkit: quality evaluation harness + parser hardening
- add internal/eval: deterministic citizen-style perturbation harness
  (typos, case, word order, abbreviations, filler words, dropped units)
- parser: strip all street-type tokens, filler words (город/дом/д/область),
  trailing apartment (кв N); handle letter-before-fraction and corpus letters
- matching: token-aware typo-tolerant street similarity; wider ambiguity
  margin to surface near-collisions instead of guessing
- seed: correct parse-rate accounting, structure-aware de-duplication
- CLI: 'addrkit eval' command; README with measured results

Measured on the real 73,936-row registry: 99.98% parsed, 0 wrong-house
safety violations across 34k+ perturbed cases.
2026-10-09 21:40:46 +07:00

167 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
},
{
name: "two type words in a registry name",
raw: "Гусинобродский Тракт улица 21",
wantName: "гусинобродский", wantType: "тракт",
wantHouse: "21", wantHas: true,
},
{
name: "name coincides with its type word",
raw: "Набережная улица 5",
wantName: "набережная", wantType: "набережная",
wantHouse: "5", wantHas: true,
},
{
name: "abbreviations dom and flat",
raw: "ул. Кошурникова, д. 11/1 кв. 5",
wantName: "кошурникова", wantType: "улица",
wantHouse: "11", wantFrac: "1", 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)
}
}
}