- remove the events/outages module, ArcGIS adapter, portal config and event statuses - add a configurable city dictionary/parser and make City part of StreetKey - seed: read city/city_name, namespace IDs by city, keep structures distinct - fix reliability: schema v2 with fail-closed load, deterministic ordering, input limits, config validation, atomic DB publish, LLM response caps - add read-only shadow mode comparing the old ES path with addrkit - breaking: registry.db must be re-seeded; config has no portal section
142 lines
4.7 KiB
Go
142 lines
4.7 KiB
Go
package address
|
|
|
|
import "testing"
|
|
|
|
func testDictionary() *Dictionary {
|
|
return NewDictionary([]City{
|
|
{Name: "Новосибирск", Aliases: []string{"нск", "новосиб", "новосибирская область", "нсо"}},
|
|
{Name: "Бердск"},
|
|
})
|
|
}
|
|
|
|
func TestParseCityFromAliases(t *testing.T) {
|
|
parser := NewParser(testDictionary(), "")
|
|
|
|
tests := []struct {
|
|
raw string
|
|
city string
|
|
nameKey string
|
|
}{
|
|
{"нск, 2-я Станционная улица 1", "Новосибирск", "станционная"},
|
|
{"НСК Ленина 5", "Новосибирск", "ленина"},
|
|
{"Новосибирская область, Станционная 1", "Новосибирск", "станционная"},
|
|
{"Бердск, Ленина 5", "Бердск", "ленина"},
|
|
{"г. Новосибирск, ул. Ленина 5", "Новосибирск", "ленина"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := parser.Parse(tt.raw)
|
|
if got.City != tt.city {
|
|
t.Errorf("%q: City = %q, want %q", tt.raw, got.City, tt.city)
|
|
}
|
|
if got.NameKey != tt.nameKey {
|
|
t.Errorf("%q: NameKey = %q, want %q", tt.raw, got.NameKey, tt.nameKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseCityIsPartOfStreetKey(t *testing.T) {
|
|
parser := NewParser(testDictionary(), "")
|
|
|
|
nsk := parser.Parse("Новосибирск, Ленина 5")
|
|
berdsk := parser.Parse("Бердск, Ленина 5")
|
|
if nsk.StreetKey == berdsk.StreetKey {
|
|
t.Fatalf("same street key for different cities: %q", nsk.StreetKey)
|
|
}
|
|
if nsk.CityKey != "новосибирск" || berdsk.CityKey != "бердск" {
|
|
t.Fatalf("city keys = %q / %q", nsk.CityKey, berdsk.CityKey)
|
|
}
|
|
}
|
|
|
|
func TestParseDefaultCity(t *testing.T) {
|
|
parser := NewParser(testDictionary(), "Новосибирск")
|
|
got := parser.Parse("Ленина 5")
|
|
if got.City != "Новосибирск" || got.CityKey != "новосибирск" {
|
|
t.Fatalf("City = %q / %q", got.City, got.CityKey)
|
|
}
|
|
|
|
// The default city is also recognised when the user spells it out, even
|
|
// if the dictionary contains only aliases.
|
|
explicit := NewParser(DefaultDictionary(), "Новосибирск").Parse("г. Новосибирск, Ленина 5")
|
|
if explicit.City != "Новосибирск" || explicit.NameKey != "ленина" {
|
|
t.Fatalf("explicit default city: %+v", explicit)
|
|
}
|
|
}
|
|
|
|
func TestParseInCityOverridesDetectedCity(t *testing.T) {
|
|
parser := NewParser(testDictionary(), "")
|
|
got := parser.ParseInCity("Бердск, Ленина 5", "Новосибирск")
|
|
if got.City != "Новосибирск" {
|
|
t.Fatalf("City = %q, want forced Новосибирск", got.City)
|
|
}
|
|
if got.NameKey != "ленина" {
|
|
t.Fatalf("NameKey = %q", got.NameKey)
|
|
}
|
|
}
|
|
|
|
func TestParseWithoutCityKeepsCityKeyEmpty(t *testing.T) {
|
|
parser := NewParser(testDictionary(), "")
|
|
got := parser.Parse("Ленина 5")
|
|
if got.City != "" || got.CityKey != "" {
|
|
t.Fatalf("City = %q / %q, want empty", got.City, got.CityKey)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeTextStillLeavesUnknownCityTokens(t *testing.T) {
|
|
// A city that is not in the dictionary must not be silently dropped: the
|
|
// resolver should fail rather than guess another city.
|
|
parser := NewParser(testDictionary(), "")
|
|
got := parser.Parse("Томск, Ленина 5")
|
|
if got.City != "" {
|
|
t.Fatalf("City = %q, want empty for unknown city", got.City)
|
|
}
|
|
if got.NameKey == "ленина" {
|
|
t.Fatalf("unknown city token disappeared from the street name")
|
|
}
|
|
}
|
|
|
|
func TestMatchCity(t *testing.T) {
|
|
dict := testDictionary()
|
|
cases := []struct {
|
|
query string
|
|
want string
|
|
ok bool
|
|
}{
|
|
{"нск", "Новосибирск", true},
|
|
{"г. Новосибирск", "Новосибирск", true},
|
|
{"Новосибирская область", "Новосибирск", true},
|
|
{"Бердск", "Бердск", true},
|
|
{"Бердск, Ленина 5", "", false},
|
|
{"Томск", "", false},
|
|
}
|
|
for _, tt := range cases {
|
|
got, ok := dict.MatchCity(tt.query)
|
|
if ok != tt.ok || got.Name != tt.want {
|
|
t.Errorf("MatchCity(%q) = %+v, %v; want %q, %v", tt.query, got, ok, tt.want, tt.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func FuzzParse(f *testing.F) {
|
|
for _, seed := range []string{
|
|
"",
|
|
"2-я Станционная улица 1/1 стр.",
|
|
"нск, Ленина 5",
|
|
"Бердск, Ленина 5 к2",
|
|
"г. Новосибирск, ул. Кошурникова, д. 11/1 кв. 5",
|
|
"\x00\xff",
|
|
} {
|
|
f.Add(seed)
|
|
}
|
|
parser := NewParser(testDictionary(), "")
|
|
f.Fuzz(func(t *testing.T, raw string) {
|
|
first := parser.Parse(raw)
|
|
second := parser.Parse(raw)
|
|
if first.StreetKey != second.StreetKey {
|
|
t.Fatalf("non-deterministic street key for %q: %q vs %q", raw, first.StreetKey, second.StreetKey)
|
|
}
|
|
if NormalizeText(raw) != NormalizeText(NormalizeText(raw)) {
|
|
t.Fatalf("NormalizeText is not idempotent for %q", raw)
|
|
}
|
|
})
|
|
}
|