fix(tradein/geocoder): фолбэк на локальные houses — часть адресов ЕКБ давала гарантированный ноль аналогов #2891

Merged
lekss361 merged 2 commits from fix/tradein-geocoder-local-house-fallback into main 2026-08-15 16:20:25 +00:00
Owner

Проблема

Из аудита 15.08. У 28 из 1084 оценок lat IS NULL — это гарантированный ноль аналогов, клиент не получает оценку вообще. За последние 30 дней таких 5 из 141.

Конкретные адреса: «ул Крестинского, д 49», «ул Онуфриева, д 24», «ул. Хрустальногорская, д. 88/2».

При этом дома есть в локальной таблице houses с координатами. Промах объясняется расхождением с ГАР: улица там «Начдива Онуфриева», а пользователь пишет «Онуфриева»; дом 49 существует только как «49к1».

Что сделано

Последней ступенью geocode() — после DaData и Nominatim, перед честным None — добавлен поиск по локальным houses с нормализацией: суффиксное сравнение имени улицы («Онуфриева» находит «Начдива Онуфриева») и достройка корпуса («49» → «49к1»). Результат помечается address_refined, признак доведён до ответа оценки.

Отдельно: DaData CLEAN отключена на аккаунте и давала logger.error на каждый пользовательский запрос — 164 события. Понижено до WARNING один раз за процесс. ERROR должен означать настоящий сбой.

Что поймало ревью

Первый круг вернул MAJOR — и по делу.

Номер квартиры подменял номер дома. Извлечение брало последнее число в строке: «ул Педагогическая, д 15, кв 11» давало дом 11 вместо 15, с confidence='exact', и результат ещё и писался в кеш. То есть оценка считалась бы по чужому зданию — тихо и уверенно. Проверено запуском на реальных строках прода.

Починено зачисткой хвоста кв/оф/пом/лит, причём шире запрошенного: добавлены подъезд/этаж, потому что реальный прод-адрес «Трамвайный переулок, 2к2, подъезд 1, этаж 25, кв. 205» ловится тем же классом ошибки. Корпус при этом не трогается — он часть номера дома.

houses — не реестр ЕКБ. 21% строк с координатами лежат вне области, вплоть до Серова и Москвы; проверено запросом. Добавлен фильтр по bbox на строку-кандидат.

Догадка про корпус была опасна. По «Начдива Онуфриева, 24» в базе есть 24к1, 24к2 и 24к3 в 250-400 метрах друг от друга — прежняя логика уверенно выбирала к1. Теперь при наличии любого другого корпуса фолбэк честно возвращает None.

Результат больше не кэшируется — координаты в houses приходят из скрейпленных объявлений и местами расходятся на километр между источниками по одному дому.

Второй круг: пять замечаний закрыты, вердикт MINOR.

Что осознанно осталось

Зачистка работает по маркеру, поэтому формы без маркера остаются: «пер Трамвайный, д 2, к 3» (корпус отрезан запятой) и «ул Белинского, д 41 (1-155)» дадут неверный токен. Живого ущерба сейчас нет — все такие адреса в проде резолвятся более ранними ступенями и до фолбэка не доходят. Полное решение требует разбора скобочных диапазонов и запятой перед корпусом; вынесу отдельно, чтобы не раздувать этот PR.

Test plan

  • 39 + новые тесты; регресс всех geocoder-тестов — 246 passed
  • три прод-адреса проверены read-only SQL: «Онуфриева, 24» и «Хрустальногорская, 88/2» резолвятся; «Крестинского, 49» честно возвращает None — в houses реально две разные строки «49к1» с разными координатами
  • после деплоя: доля lat IS NULL за неделю должна упасть
## Проблема Из аудита 15.08. У 28 из 1084 оценок `lat IS NULL` — это гарантированный ноль аналогов, клиент не получает оценку вообще. За последние 30 дней таких 5 из 141. Конкретные адреса: «ул Крестинского, д 49», «ул Онуфриева, д 24», «ул. Хрустальногорская, д. 88/2». При этом дома **есть** в локальной таблице `houses` с координатами. Промах объясняется расхождением с ГАР: улица там «Начдива Онуфриева», а пользователь пишет «Онуфриева»; дом 49 существует только как «49к1». ## Что сделано Последней ступенью `geocode()` — после DaData и Nominatim, перед честным `None` — добавлен поиск по локальным `houses` с нормализацией: суффиксное сравнение имени улицы («Онуфриева» находит «Начдива Онуфриева») и достройка корпуса («49» → «49к1»). Результат помечается `address_refined`, признак доведён до ответа оценки. Отдельно: DaData CLEAN отключена на аккаунте и давала `logger.error` на **каждый** пользовательский запрос — 164 события. Понижено до WARNING один раз за процесс. ERROR должен означать настоящий сбой. ## Что поймало ревью Первый круг вернул **MAJOR** — и по делу. **Номер квартиры подменял номер дома.** Извлечение брало последнее число в строке: «ул Педагогическая, д 15, кв 11» давало дом **11** вместо 15, с `confidence='exact'`, и результат ещё и писался в кеш. То есть оценка считалась бы по чужому зданию — тихо и уверенно. Проверено запуском на реальных строках прода. Починено зачисткой хвоста `кв/оф/пом/лит`, причём шире запрошенного: добавлены `подъезд/этаж`, потому что реальный прод-адрес «Трамвайный переулок, 2к2, подъезд 1, этаж 25, кв. 205» ловится тем же классом ошибки. Корпус при этом не трогается — он часть номера дома. **`houses` — не реестр ЕКБ.** 21% строк с координатами лежат вне области, вплоть до Серова и Москвы; проверено запросом. Добавлен фильтр по bbox на строку-кандидат. **Догадка про корпус была опасна.** По «Начдива Онуфриева, 24» в базе есть 24к1, 24к2 и 24к3 в 250-400 метрах друг от друга — прежняя логика уверенно выбирала к1. Теперь при наличии любого другого корпуса фолбэк честно возвращает `None`. **Результат больше не кэшируется** — координаты в `houses` приходят из скрейпленных объявлений и местами расходятся на километр между источниками по одному дому. Второй круг: пять замечаний закрыты, вердикт **MINOR**. ## Что осознанно осталось Зачистка работает по маркеру, поэтому формы **без** маркера остаются: «пер Трамвайный, д 2, к 3» (корпус отрезан запятой) и «ул Белинского, д 41 (1-155)» дадут неверный токен. Живого ущерба сейчас нет — все такие адреса в проде резолвятся более ранними ступенями и до фолбэка не доходят. Полное решение требует разбора скобочных диапазонов и запятой перед корпусом; вынесу отдельно, чтобы не раздувать этот PR. ## Test plan - [x] 39 + новые тесты; регресс всех geocoder-тестов — 246 passed - [x] три прод-адреса проверены read-only SQL: «Онуфриева, 24» и «Хрустальногорская, 88/2» резолвятся; «Крестинского, 49» честно возвращает `None` — в `houses` реально две разные строки «49к1» с разными координатами - [ ] после деплоя: доля `lat IS NULL` за неделю должна упасть
lekss361 added 2 commits 2026-08-15 16:01:50 +00:00
28/1084 прод-оценок имели lat IS NULL — гарантированный ноль аналогов, клиент
не получал оценку вовсе. Дом уже был в houses (скрейпленные листинги), но не
резолвился ни geoportal/cad_buildings, ни Nominatim: разговорное/усечённое имя
улицы («Онуфриева» вместо ГАР-каноничного «Начдива Онуфриева») или отсутствующий
в вводе корпус («49» вместо реального «49к1»). Добавлен последний тир geocode()
с двумя defensive-допущениями (суффиксный матч улицы + опциональная догадка
«номер+к1») — при любой неоднозначности возвращает None, а не гадает; проверено
живыми прод-адресами (Онуфриева/Хрустальногорская резолвятся, Крестинского
корректно остаётся неоднозначным — два разных дома в houses под одним номером).

Отдельно: HTTP 403 «услуга CLEAN выключена на аккаунте» логировался как ERROR
на каждый /estimate (164 события) — это статичная конфигурация аккаунта, а не
сбой; понижено до WARNING (первый раз за процесс) + DEBUG на повторы, чтобы
ERROR продолжал значить настоящую проблему.
fix(tradein/geocoder): stop apt number leaking into house + houses bbox/sibling guard
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 5m0s
fa84705ec7
Review round 2 on #2626 (local houses fallback) found two HIGH-severity bugs
verified live against prod data:

1. _extract_local_house_token took the LAST digit-like token in the raw
   address, so "...Педагогическая, д 15, кв 11" resolved house=11 (apartment
   number) instead of 15 -- confidently returning a stranger's building with
   confidence='exact', written to geocode_cache. Fixed by stripping the
   apartment/office/floor/entrance tail (кв/оф/пом/подъезд/этаж -- NOT
   корп/к, which is part of the house number) before extracting the token.
   Fixes the exact prod case from the review plus the corpus+apartment
   combo ("д 26 к 1, кв 41" -> 26к1, not 41).

2. houses is not an EKB-only table (21% of rows with coords are outside the
   metro, some as far as another city) -- "улица Маяковского, 7" in houses
   resolves to Серов, not Екатеринбург, and use_local_ekb only gates the
   user's query text, not the source row. Added an is_within_ekb_bbox_wide
   check on every candidate row before it can become a match.

Also addressed two MEDIUM findings from the same review:

3. The "<номер> -> <номер>к1" corpus guess only checked uniqueness among
   к1-labelled rows, so real multi-building addresses (Онуфриева 24: к1/к2/к3,
   250-400m apart) resolved confidently to к1 anyway. Guess is now skipped
   when any other corpus/slash variant of the same base number exists among
   the street's candidates.

4. Houses-fallback results are no longer cached in geocode_cache -- the
   source (scraped listings) is less reliable than geoportal/cadastral/
   Nominatim, and the lookup is cheap/local, so caching only extended the
   lifetime of a possible bad match. Side benefit: address_refined now
   survives every repeat request of the same raw address, not just the
   first.

Also added ORDER BY address, id to the underlying query so the coordinate
dedup picks a deterministic row (LOW finding #5).

14 new/updated tests in test_geocoder_local_houses_fallback.py cover all
five findings against real prod address/houses-row fixtures. Full geocoder
+ dadata + estimator/pdf regression suite (402 tests) green.
lekss361 merged commit 8c7d2d4d10 into main 2026-08-15 16:20:25 +00:00
lekss361 deleted branch fix/tradein-geocoder-local-house-fallback 2026-08-15 16:20:25 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#2891
No description provided.