fix(tradein/geocoder): fail-closed выбор ЕКБ-реестров вместо списка городов (#2582) #2589

Merged
lekss361 merged 3 commits from fix/tradein-geocoder-fail-closed into main 2026-07-31 16:30:39 +00:00
Owner

Closes #2582. Второй критический дефект аудита — выбор екатеринбургских справочников держался на списке из 37 названий: всё, чего в списке нет, молча считалось Екатеринбургом.

Подтверждено живыми данными прода: в кэше геокодера лежали «Ялта», «Трёхгорный» (Челябинская обл.) и посёлок Лёвиха с координатами Екатеринбурга, все с пометкой «точно».

Решение — два независимых механизма вместо перечисления городов

Новый _ekb_local_tiers_allowed(address, city_hint), три сигнала по ИЛИ, каждый способен закрыть доступ к екатеринбургским реестрам:

  1. известный город региона в тексте (прежняя логика);
  2. известный не-екатеринбургский город в city_hint (пришло из #2580);
  3. новое_names_unrecognized_locality(): структурный признак топонима («обл.», «область», «край», «республика», «г.», «пос.», «с.») в тексте.

Третий механизм и закрывает корень: он ловит любой чужой топоним, не требуя знать его по имени. Именно поэтому Ялта и Трёхгорный больше не попадут в екатеринбургский реестр, хотя их в списке нет и не будет.

Признак «д.» намеренно не включён — это сокращение дома («ул. Ленина, д. 5»), его включение закрыло бы гейт почти на каждом адресе.

Ключевое различение

«Город не назван вообще» (адрес «Малышева 30» — основной трафик формы) → доступ открыт, Екатеринбург как рабочая гипотеза. «Назван незнакомый город» → закрыто. Раньше оба случая трактовались одинаково — отсюда и дефект.

Слияние с уже влитым #2580

Оба конфликта были в одном месте — гейте use_local_ekb. Логика сведена в один хелпер, оба вызывающих места передают хинт вторым аргументом без дублирования условия. Все четыре ветки проверены тестами (хинт с чужим городом / незнакомый топоним / ничего не названо / явный city_hint="Екатеринбург"); на последнюю добавлен интеграционный тест — она не была покрыта ни одним из наборов.

Задокументирован унаследованный компромисс: поскольку это ИЛИ, а не приоритетная цепочка, хинт, противоречащий явному «Екатеринбург» в тексте, всё равно закроет гейт. Семантика #2580 не расширялась и не сужалась.

Проверки

Оба набора тестов зелёные одновременно: 148 passed по трём файлам геокодера. Полный прогон — 2914 passed, 1 pre-existing. Доказательство до слияния: откат только рабочего кода → падают 6 тестов на Ялту/Трёхгорный/Лёвиху/Реж, при этом 3 екатеринбургских проходят и на старом коде.

Известное ограничение: незнакомый город без всяких приставок («Реж, Ленина 5» без «Свердловская обл.») по-прежнему уйдёт в Екатеринбург — осознанный компромисс, иначе сломался бы основной сценарий ввода без города. Все реальные испорченные записи из прода содержали региональную приставку.

Closes #2582. Второй критический дефект аудита — выбор екатеринбургских справочников держался на списке из 37 названий: **всё, чего в списке нет, молча считалось Екатеринбургом**. Подтверждено живыми данными прода: в кэше геокодера лежали «Ялта», «Трёхгорный» (Челябинская обл.) и посёлок Лёвиха **с координатами Екатеринбурга**, все с пометкой «точно». ## Решение — два независимых механизма вместо перечисления городов Новый `_ekb_local_tiers_allowed(address, city_hint)`, три сигнала по ИЛИ, каждый способен закрыть доступ к екатеринбургским реестрам: 1. известный город региона в тексте (прежняя логика); 2. известный не-екатеринбургский город в `city_hint` (пришло из #2580); 3. **новое** — `_names_unrecognized_locality()`: структурный признак топонима («обл.», «область», «край», «республика», «г.», «пос.», «с.») в тексте. Третий механизм и закрывает корень: он ловит **любой** чужой топоним, не требуя знать его по имени. Именно поэтому Ялта и Трёхгорный больше не попадут в екатеринбургский реестр, хотя их в списке нет и не будет. Признак «д.» **намеренно не включён** — это сокращение дома («ул. Ленина, д. 5»), его включение закрыло бы гейт почти на каждом адресе. ## Ключевое различение «Город не назван вообще» (адрес «Малышева 30» — основной трафик формы) → доступ открыт, Екатеринбург как рабочая гипотеза. «Назван незнакомый город» → закрыто. Раньше оба случая трактовались одинаково — отсюда и дефект. ## Слияние с уже влитым #2580 Оба конфликта были в одном месте — гейте `use_local_ekb`. Логика сведена в один хелпер, оба вызывающих места передают хинт вторым аргументом без дублирования условия. **Все четыре ветки проверены тестами** (хинт с чужим городом / незнакомый топоним / ничего не названо / явный `city_hint="Екатеринбург"`); на последнюю добавлен интеграционный тест — она не была покрыта ни одним из наборов. Задокументирован унаследованный компромисс: поскольку это ИЛИ, а не приоритетная цепочка, хинт, противоречащий явному «Екатеринбург» в тексте, всё равно закроет гейт. Семантика #2580 не расширялась и не сужалась. ## Проверки Оба набора тестов зелёные одновременно: 148 passed по трём файлам геокодера. Полный прогон — 2914 passed, 1 pre-existing. Доказательство до слияния: откат только рабочего кода → падают 6 тестов на Ялту/Трёхгорный/Лёвиху/Реж, при этом 3 екатеринбургских проходят и на старом коде. **Известное ограничение:** незнакомый город без всяких приставок («Реж, Ленина 5» без «Свердловская обл.») по-прежнему уйдёт в Екатеринбург — осознанный компромисс, иначе сломался бы основной сценарий ввода без города. Все реальные испорченные записи из прода содержали региональную приставку.
lekss361 added 2 commits 2026-07-31 15:49:21 +00:00
EKB-only локальные тиры (geoportal/cad_buildings) держались на списке из 37
городов области: "нет в списке -> считаем Екатеринбургом". Молча ломало любой
адрес вне списка, включая ДРУГИЕ РЕГИОНЫ РФ целиком -- подтверждено живыми
случаями в geocode_cache (Ялта/Крым, Трёхгорный/Челябинская обл., пос. Лёвиха,
Димитровград/Ульяновская обл. -- все получили координаты ЕКБ).

Новый гейт `_ekb_local_tiers_allowed`: локальные тиры разрешены только если
Екатеринбург подтверждён явно, ЛИБО в адресе вообще не назван город/регион
(bare "Ленина 1" -- основной трафик формы оценки, дефолт ЕКБ оправдан и
сохранён без изменений). Если назван ЛЮБОЙ другой топоним -- известный город
списка (`_names_non_ekb_city`, без изменений) или незнакомый
(`_names_unrecognized_locality` -- новый структурный маркер "обл./край/
республика/г./пос./с.", БЕЗ "д." -- коллизия с house-маркером "д. 5") -- гейт
закрыт, идём во внешние oblast/nation-aware провайдеры.

city_hint-логика #2580 не тронута (тот же файл, сосед по цели).
Merge forgejo/main into fix/tradein-geocoder-fail-closed
All checks were successful
CI / changes (pull_request) Successful in 12s
CI Trade-In / changes (pull_request) Successful in 13s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m35s
174680a82e
Разрешён конфликт в geocoder.py между #2580 (city_hint threading в
локальный гейт + dual-query Tier 4 Nominatim) и #2582 (fail-closed
структурный маркер топонима вместо списка городов): `_ekb_local_tiers_allowed`
теперь принимает `city_hint` и закрывает гейт по OR трёх независимых
сигналов — известный не-ЕКБ город в тексте (#11), known-city в city_hint
(#2580 C1), незнакомый топоним-маркер в тексте (#2582). Добавлены тесты на
все 4 достижимые ветки (hint=известный город / маркер в тексте / ничего /
hint=Екатеринбург); полный набор тестов (свои + пришедшие из #2580) зелёный
одновременно.
bot-backend added 1 commit 2026-07-31 16:20:33 +00:00
fix(tradein/geocoder): починить маркер села + fail-closed для явного city_hint (#2582)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m20s
af609c18f3
H1 (deep-review PR #2589): `с\.` в _LOCALITY_MARKER_RE был мертворождён —
хвостовой \b не может сработать между "." и пробелом (оба не-словесные),
формат «с. <село>» (104/77к прод-адресов) не ловился НИ РАЗУ, зато ложно
срабатывал на инициалах «им с.п.королева». Живой инцидент воспроизведён:
"с. Новоалексеевское, ул. 8 Марта,37" снапалось в ЕКБ (совпадение до 5-го
знака, село в 35 км западнее). Заменил на lookahead-форму для однобуквенных
маркеров (с/д/дер/пгт/ст): "\.\s*(?=[а-яё])" — различает "д. 5" (дом-номер,
не матчит) от "д. Кашино" (деревня, матчит), что заодно позволило безопасно
включить "д."/"дер." (раньше исключённые из-за коллизии с house-маркером).

M2: гейт по city_hint оставался list-based (_names_non_ekb_city(city_hint)) —
для СТРУКТУРНОГО хинта (не свободного текста) fail-closed форма должна быть
обратной: "не Екатеринбург → закрыто", а не "есть в списке 37 → закрыто".
city_hint принимает произвольную строку без валидации (API напрямую, минуя
6-городный UI-selector) — city_hint="Реж" держал гейт открытым, geoportal
отдавал точный ЕКБ-дом, city_ambiguous=False делало это уверенно-неверным.

M3: добавлены изолирующие тесты — предыдущая параметризация "незнакомый
топоним" всегда несла ещё и "обл."/"республика"/"край" одновременно с
проверяемым сигналом, поэтому мёртвый "с\." прошёл незамеченным. Плюс честно
зафиксирована известная дыра (гейт по city_hint) — "реж, ленина 5" без
маркера остаётся в ЕКБ-дефолте (архитектурное решение отдельно, не список
городов).

docstring `_names_unrecognized_locality`/`_ekb_local_tiers_allowed` приведён
в соответствие с фактическим поведением regex.
lekss361 merged commit fdf222cc5c into main 2026-07-31 16:30:39 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2589
No description provided.