fix(tradein/geocoder): не подставлять Екатеринбург молча — явный city_hint (#2576) #2580

Merged
lekss361 merged 2 commits from fix/tradein-geocoder-city-default into main 2026-07-31 15:23:35 +00:00
Owner

Эпик #2576, этап B. Самый опасный из тихих дефектов. Геокодер молча добавлял префикс "Екатеринбург, {адрес}", если в тексте не было маркера города — а фронт город не передаёт вовсе. Житель Нижнего Тагила, вводя «Ленина, 1», получал координаты екатеринбургской улицы и оценку по чужому рынку с обычной уверенностью. Прецедент уже был: Тагил оценивался в 187 500 ₽/м² вместо реальных ~84 000.

Решение: приоритет разрешения города — маркер в самом адресе → явный city_hintничего (вместо молчаливого Екатеринбурга). Запрос уходит без префикса города, а географический bias смещается с EKB_BBOX на OBLAST66_VIEWBOX (вся область). Строку «Свердловская область, …» не подставляем: текстовому поиску она ничего не даёт (bbox уже задан), зато не порождает мусор вида «Екатеринбург, Нижний Тагил, Ленина 10».

Честный признак неопределённости: GeocodeResult.city_ambiguous — «город определил провайдер, а не пользователь», проставляется независимо от того, какой тир дал результат, и пробрасывается в ответ оценки как target_city_ambiguous. Поле эфемерное, миграции не требует.

API: city_hint — опциональный query-параметр на /lookup и /suggest, плюс поле во входе оценки. Контракт не сломан.

ЕКБ не деградирует: при явном маркере города или переданном city_hint поведение прежнее.

Кэш: ключ становится "{адрес}|city={hint}" только когда hint передан, иначе прежний — обратная совместимость сохранена. NB: пока фронт не шлёт city_hint, одинаковые адреса разных городов по-прежнему неразличимы по ключу — это структурное ограничение, снимается фронтовой частью. Рекомендована точечная чистка geocode_cache: записи provider IN ('yandex','nominatim') с екатеринбургским full_address, где исходный адрес не содержал маркера города (аналог чистки 614 строк после #2504). Чистку не выполнял — отдельным шагом.

Тесты: +23 (test_geocoder_city_hint.py), на старом коде падают (ImportError: _cache_key). Полный прогон: 2849 passed, 1 pre-existing fail. ruff чисто.

Следующий шаг (отдельный PR): фронт должен передавать город — до этого city_ambiguous будет истинным почти всегда для адресов без города в тексте. Это честно, но UI пока не использует флаг.

Эпик #2576, этап B. **Самый опасный из тихих дефектов.** Геокодер молча добавлял префикс `"Екатеринбург, {адрес}"`, если в тексте не было маркера города — а фронт город не передаёт вовсе. Житель Нижнего Тагила, вводя «Ленина, 1», получал координаты екатеринбургской улицы и оценку по чужому рынку с обычной уверенностью. Прецедент уже был: Тагил оценивался в 187 500 ₽/м² вместо реальных ~84 000. **Решение:** приоритет разрешения города — маркер в самом адресе → явный `city_hint` → **ничего** (вместо молчаливого Екатеринбурга). Запрос уходит без префикса города, а географический bias смещается с `EKB_BBOX` на `OBLAST66_VIEWBOX` (вся область). Строку «Свердловская область, …» не подставляем: текстовому поиску она ничего не даёт (bbox уже задан), зато не порождает мусор вида «Екатеринбург, Нижний Тагил, Ленина 10». **Честный признак неопределённости:** `GeocodeResult.city_ambiguous` — «город определил провайдер, а не пользователь», проставляется независимо от того, какой тир дал результат, и пробрасывается в ответ оценки как `target_city_ambiguous`. Поле эфемерное, миграции не требует. **API:** `city_hint` — опциональный query-параметр на `/lookup` и `/suggest`, плюс поле во входе оценки. Контракт не сломан. **ЕКБ не деградирует:** при явном маркере города или переданном `city_hint` поведение прежнее. **Кэш:** ключ становится `"{адрес}|city={hint}"` только когда hint передан, иначе прежний — обратная совместимость сохранена. NB: пока фронт не шлёт `city_hint`, одинаковые адреса разных городов по-прежнему неразличимы по ключу — это структурное ограничение, снимается фронтовой частью. Рекомендована точечная чистка `geocode_cache`: записи `provider IN ('yandex','nominatim')` с екатеринбургским `full_address`, где исходный адрес не содержал маркера города (аналог чистки 614 строк после #2504). **Чистку не выполнял** — отдельным шагом. Тесты: +23 (`test_geocoder_city_hint.py`), на старом коде падают (`ImportError: _cache_key`). Полный прогон: 2849 passed, 1 pre-existing fail. ruff чисто. **Следующий шаг (отдельный PR):** фронт должен передавать город — до этого `city_ambiguous` будет истинным почти всегда для адресов без города в тексте. Это честно, но UI пока не использует флаг.
lekss361 added 1 commit 2026-07-31 13:44:05 +00:00
fix(tradein/geocoder): не подставлять Екатеринбург молча — явный city_hint (#2576)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 10s
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 2m17s
4b1a282234
Раньше _yandex_lookup/_yandex_suggest/_nominatim_suggest молча подставляли
"Екатеринбург, " в запрос, если в адресе не было маркера города/области.
Житель Нижнего Тагила, вводя «Ленина, 1», получал уверенно неверную цену по
екатеринбургской улице Ленина (обе улицы называются одинаково) — фронт город
вообще не передаёт.

- geocode()/suggest() принимают опциональный city_hint: str | None; без него
  внешние тиры больше НЕ подставляют город, а bias (ll/spn) смещается на всю
  область (OBLAST66_VIEWBOX) вместо ЕКБ-центра. Явный маркер города в адресе
  или city_hint сохраняют прежнее поведение (ЕКБ-путь не деградирует).
- GeocodeResult.city_ambiguous — честный флаг «город определил провайдер, а
  не пользователь» (не эвристика на корректность), проброшен в
  AggregatedEstimate.target_city_ambiguous (ephemeral, не персистится).
- Cache-ключ geocode_cache учитывает city_hint (address|city=...) — без hint'а
  формат не меняется (backward-compat), с hint'ом разные города для одного
  текста адреса больше не делят одну запись.
- API: /api/v1/geocode/lookup, /suggest и POST /trade-in/estimate получили
  опциональный city_hint — контракт не ломается (default None).

23 новых теста в test_geocoder_city_hint.py; проверено что они падают
(ImportError на _cache_key) на коде до фикса через git stash.
bot-backend added 1 commit 2026-07-31 14:57:40 +00:00
fix(tradein/geocoder): city_hint должен доходить до локальных тиров + не терять ЕКБ-привязку в Tier 4 (#2576)
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / changes (pull_request) Successful in 9s
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 2m19s
b3c76c8c62
Deep-review PR #2580 нашёл два блокера в предыдущем фиксе (#2576):

C1 — city_hint не участвовал в гейте локальных ЕКБ-only тиров
(geoportal/cadastral, `use_local_ekb` в geocode() и Tier 1 в suggest()).
Явный city_hint="Нижний Тагил" для "Ленина 1" всё равно попадал на ЕКБ-only
базы, получал "точный" ЕКБ-хит и при этом city_ambiguous=False (хинт ведь
был) — то есть система теперь ложно-уверенно утверждала неверный город.
Фикс: city_hint участвует в той же проверке _names_non_ekb_city (гейт/
gazetteer #2582 не трогаю — только добавляю вход).

C2 — снятие суффикса ", Екатеринбург" в _nominatim_suggest для случая
"город неизвестен" регрессило часть реальных ЕКБ-адресов: без текстовой
подсказки о городе Nominatim иногда предпочитает street-level матч в
соседнем городе-спутнике (эмпирика ревьюера: "Победы 20" без суффикса →
Верхняя Пышма вместо ЖК "Парк Победы" в Екатеринбурге). Решение — dual-query:
bare (честный, без города) И ЕКБ-suffixed запросы объединяются (не заменяют
друг друга), оба честных кандидата остаются в подсказках, пользователь
выбирает сам. Extra round-trip только для последнего fallback-тира
(cadastral/DaData/Yandex уже не сработали) — не задевает основной трафик.

Заодно (🟠, дешёвая правка): city_hint прокинут в _nominatim_lookup — с тех
пор как Yandex-ключ недействителен (#2585), это единственный живой внешний
провайдер, и его tie-break (предпочитает tight-ЕКБ bbox) без города не
различает одноимённые улицы внутри региона.

4 новых теста (C1×2, C2×2 + dedupe) — проверено что все 4 падают на коде до
этого коммита через git stash (только geocoder.py, тесты оставлены).
lekss361 merged commit 8d3b571925 into main 2026-07-31 15:23:35 +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#2580
No description provided.