fix(tradein/geocoder): строгий матч литеры дома + починка DaData region-фильтра #2622

Merged
lekss361 merged 2 commits from fix/tradein-geocoder-house-letter into main 2026-08-02 12:06:45 +00:00

2 commits

Author SHA1 Message Date
bot-backend
9f03733e46 docs(tradein/geocoder): исправить докстринг — в geocode() нет тира DaData
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / 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 2m48s
Докстринг _cadastral_house_match утверждал, что промах уходит в цепочку
_cadastral_forward_sync → DaData → Nominatim. Тира DaData в geocode() нет:
_dadata_suggest вызывается ровно в одном месте — внутри suggest() (автокомплит).

Реальные цепочки разведены явно, плюс зафиксировано следствие: на прямом
вызове geocode() адрес с литерой, неизвестный геопорталу и Nominatim, даёт
None вместо оценки соседнего дома. Основной UI-путь не задет — координаты
приходят из выбранной подсказки и минуют geocode().
2026-08-02 14:56:27 +03:00
bot-backend
d63702cfec fix(tradein/geocoder): литера дома — часть идентичности, а не tie-break
All checks were successful
CI / changes (pull_request) Successful in 8s
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 2m44s
`_cadastral_house_match` сравнивал дом только по ЦИФРАМ: литера была
опциональна в WHERE (`[а-яё]?`) и участвовала лишь как tie-break в
ORDER BY. Баг двусторонний и молчаливый:

  «Новгородцевой 13б» → «дом 13»   (дома 13б в реестре нет вообще)
  «Малышева 30»       → «д. 30-б»  (обратное направление, живой прод-кейс)

Оба результата возвращались с `confidence="exact"` и оседали в
`geocode_cache` на 90 дней — пользователь получал оценку ЧУЖОГО здания,
помеченную как точная.

Почему так: номер дома теперь сравнивается РАВЕНСТВОМ нормализованных
форм. Обе стороны приводятся к одному канону (`13б` / `13 б` / `13-б` /
`13Б` → `13б`): запрос — существующим `_norm_house`, реестр — тем же
выражением на стороне Postgres. Дешёвый regex-anchor по цифрам оставлен
prefilter'ом (пушится в FDW, режет «(1-83)»-диапазоны), но корректность
теперь на равенстве, а не на нём.

Побочно закрыты той же логикой:
  • «58» больше не матчит «58/3», «13» не матчит «130» — угловой номер
    это часть номера дома, а не мусор;
  • маркер дома ищется только с начала слова (`\m`), иначе «проезд 8
    Марта, д 5» давал дом «8» — старый `д\.?` ловил «д» внутри «проезд»
    (20 таких строк реестра были достижимы ТОЛЬКО через этот баг);
  • номер дома больше не конкатенируется в regex — regex-injection
    поверхность сузилась до цифр prefilter'а.

Промаха не превращаем в тихую подстановку соседа: нет дома с нужной
литерой → None, дальше отрабатывают следующие тиры (forward → DaData →
Nominatim). Понижать confidence здесь нечем — `GeocodeSuggestion` его не
несёт, все локальные тиры хардкодят "exact"; честный фолбэк дешевле, чем
протаскивать новый канал уверенности через три тира.

DaData-тир, который должен подхватывать такие адреса, был мёртв: в
`locations` уходило `region="Свердловская область"`, тогда как DaData
хранит имя региона БЕЗ типа (`region="Свердловская"`, `region_type="обл"`).
Hard-filter не совпадал ни с чем и молча схлопывал выдачу в 0 подсказок
(замер на проде: 0 хитов против 5 с «Свердловская», первый — искомый
«д 13б» с fias_id). Добавлен warning на пустую выдачу под region-
констрейнтом, чтобы следующая такая регрессия не была невидимой.

Миграция 203 инвалидирует уже отравленные записи `geocode_cache` —
точечно (запрос с литерой, либо запрос без литеры с закэшированным
домом С литерой), а не весь кэш: полная очистка сожгла бы квоту DaData
на ре-резолв заведомо корректных адресов.

Проверено: SQL-выражение прогнано против живого реестра (47k строк) —
«13б»→∅, «13»→дом 13, «30»→сооружение 30 (не «30-б»), «23б»→«д. 23-б»;
предикат миграции — против таблицы кейсов (4 delete / 8 keep).
Полный pytest: 1 failed, 3123 passed — падение
`test_search_api.py::test_search_cache_hit` (401) воспроизводится и на
чистом main (baseline 1 failed, 3081 passed), к этому изменению
отношения не имеет.

НЕ проверено вживую: ответ DaData с новым region-констрейнтом (нет
доступа к прод-токену из этой сессии) — опираюсь на замер из отчёта об
issue и на модель данных DaData.
2026-08-02 13:30:16 +03:00