fix(tradein/geocoder): gate EKB-local tiers for non-EKB oblast addresses (#11) #2504
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2504
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/geocoder-non-ekb-city-leak"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Root cause (#11):
geocode()ran EKB-only local tiers (2a_geoportal_house_matchon
ekb_geoportal_buildings, 2c_cadastral_house_match, 2d_cadastral_forward_sync) BEFORE the external providers._parse_street_housedrops the city, so any non-EKB Sverdlovsk-oblast street+house that collides
with an EKB building (e.g. "проспект Ленина, 1" exists in both Nizhny Tagil and EKB)
short-circuited to EKB coordinates before ever reaching Yandex/Nominatim.
Note: as of this branch's base,
forgejo/mainalready ships oblast-wideawareness for the EXTERNAL providers (
OBLAST66_BBOX,OBLAST66_VIEWBOX,_has_oblast_marker, Yandex/Nominatim region cross-check) from a same-dayprior commit (
c0cbdc2f). That fix solved "valid oblast geocodes getrejected" but did NOT gate the EKB-only LOCAL DB tiers, which is the bug
this PR closes.
Change
_names_non_ekb_city(address) -> bool— reuses the existingSVERDLOVSK_OBLAST_CITIESgazetteer (minus Екатеринбург) and theword-boundary matching + district-prefix exclusion already used by
_has_oblast_marker(no new gazetteer/normalization scheme introduced).geocode(): computeuse_local_ekb = not _names_non_ekb_city(address)right after
parsed = _parse_street_house(...); gate tiers 2a/2c/2d behindit. Non-EKB-city addresses skip straight to the already oblast-aware
Yandex/Nominatim tiers (3/4) below.
_names_non_ekb_cityisFalse(EKB address or bare/ambiguous address),use_local_ekbisTrueand the code path is byte-identical in ordering/logic to before this PR
— only additive gating.
gendesign_cad_buildingsis 99.85% EKB-scoped(47043/47111 rows; only 1 real non-EKB row found),
ekb_geoportal_buildingsis 100% EKB by construction — gating loses no meaningful non-EKB local
coverage.
Tests
tests/test_geocoder_bbox.py: unit tests for_names_non_ekb_city(Nizhny Tagil → True, EKB-prefixed → False, bare address → False,
"Серова 27" word-boundary → False, district-prefix → False,
"Каменск-Уральский" → True).
tests/test_geocoder_cadastral_matcher.py:geocode()wiring test — for aNizhny Tagil address,
_geoportal_house_match/_cadastral_house_match/_cadastral_forward_syncare NOT called; result comes from a stubbed_nominatim_lookupinstead.ruff check/ruff format --check/pytest tests -k geocod -qall green(160 passed).
Follow-up (NOT in this PR)
geocode_cachehas rows poisoned by the old behavior — non-EKB addressescached with wrong EKB coordinates from a prior lookup. A cache purge of those
rows is a required follow-up; main session will handle it (out of scope for
this PR per task instructions — no SQL/migrations touched here).
Deep-review fast-follows on the non-EKB city gate: 1. Word-boundary the "екатеринбург" short-circuit in _names_non_ekb_city (new _EKATERINBURG_RE = \bекатеринбург\b, .search() instead of substring `in`). A bare-substring check matched "Екатеринбургское шоссе" (a real street in satellite towns like Pervouralsk) as if it named EKB, wrongly keeping EKB-only local tiers on for a non-EKB address. Still returns False for the existing homonym false-positive test case (word boundary is satisfied there — "екатеринбург" appears as a standalone token). 2. Gate suggest()'s Tier 1 (same root cause as geocode()): the EKB-only cadastral matchers _cadastral_house_match / _cadastral_forward_sync ran unconditionally, so a non-EKB oblast autocomplete query could surface an EKB building via street+house collision. Gated behind `not _names_non_ekb_city(query)`, mirroring geocode()'s use_local_ekb. External suggest tiers (DaData/Yandex/Nominatim) untouched — still the path for non-EKB autocomplete. 3. Extended SVERDLOVSK_OBLAST_CITIES with unambiguous, deal-heavy oblast cities: алапаевск, сухой лог, кушва, красноуральск, карпинск, нижняя тура, верхний тагил, нижние серги. Deliberately did NOT add "лесной" (reviewer flagged as reviewer-optional, left to judgment): DB check (ekb_geoportal_buildings) confirms a real EKB street named exactly "Лесной" (3 buildings) — an exact whole-word collision indistinguishable from ЗАТО Лесной by word-boundary matching alone (unlike "Серова"/"Серов", which are different word forms). Adding it would misclassify a bare EKB address ("Лесной, 5", no "Екатеринбург" mention) as non-EKB. Tests: word-boundary EKB regression test, suggest() gating test, two new gazetteer entries (Верхняя Пышма multi-word, Сухой Лог).