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
Owner

Проблема

_cadastral_house_match сравнивал дом только по цифрам: литера была опциональна в WHERE ([а-яё]?) и участвовала лишь как tie-break в ORDER BY. Баг двусторонний и молчаливый:

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

Оба результата шли с confidence="exact" и оседали в geocode_cache на 90 дней → пользователь получал оценку чужого здания, помеченную как точная.

Что сделано

1. Строгое равенство номера дома. Обе стороны приводятся к одному канону (13б / 13 б / 13-б / 13Б13б): запрос — существующим _norm_house, реестр — тем же выражением на стороне Postgres (_SQL_HOUSE_TOKEN_RE + _SQL_HOUSE_TOKEN_NORM). Дешёвый regex-anchor по цифрам оставлен prefilter'ом (пушится в FDW, режет «(1-83)»-диапазоны) — корректность теперь на равенстве, а не на нём.

Формы литеры взяты из реальных данных, не выдуманы — замер по gendesign_cad_buildings (47k строк):

форма строк
д. 13 / дом 13 / сооружение 30 20 948
д. 13б 2 646
д. 13-б 2 093
д. 58/3, д. 64-2 (угловые) 922
д. 13 б 125
д. 102 корпус 1 60
д. 11 (кв. 1-150) / (литера А) 49 (из них литера1, проигнорирована)

Побочно закрыто той же логикой:

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

2. Промах ≠ тихая подстановка соседа. Нет дома с нужной литерой → None, дальше отрабатывают следующие тиры.

⚠️ Исправлено 02.08. В первой редакции здесь была указана цепочка _cadastral_forward_sync → DaData → Nominatim. Тира DaData в geocode() нет_dadata_suggest вызывается ровно в одном месте, внутри suggest() (автокомплит). Реальная цепочка geocode(): cache → geoportal → cadastral → _cadastral_forward_sync → Nominatim → None.

Следствие, принятое сознательно: на прямом вызове geocode() (API, PDF, восстановление по ?id=) адрес с литерой, неизвестный ни геопорталу, ни Nominatim, вернёт None — оценка не построится, тогда как раньше строилась по соседнему дому. Честный отказ вместо уверенно-неверной оценки с меткой exact.

Основной UI-путь этим не задет: координаты берутся из выбранной подсказки (ParamsPanel.tsx:776api/v1/trade_in.py:128 использует lat/lon напрямую), geocode() в этом сценарии не участвует. Докстринг _cadastral_house_match исправлен в 9f03733e.

Почему None, а не пониженный confidence: GeocodeSuggestion вообще не несёт confidence — его хардкодит geocode() ("exact" для всех трёх локальных тиров). Протаскивать новый канал уверенности через три тира ради этого случая — несоразмерно; фолбэк на внешние тиры и честнее, и дешевле. Проверено, что промах не утекает в соседний тир: _cadastral_forward_sync матчит литеральной подстрокой всего запроса, и «Новгородцевой 13б» не совпадает с «…Новгородцевой, дом 13» → 0 хитов → идём в DaData.

3. DaData-тир глушился фильтром региона. В locations уходило region="Свердловская область", тогда как DaData хранит имя региона без типа (region="Свердловская", тип отдельно в region_type="обл"). locations — hard-filter, не boost: он не совпадал ни с чем и молча схлопывал выдачу в 0 подсказок без всякой ошибки. Именно поэтому фолбэк для «13б» не спасал.

Добавлен logger.warning на пустую выдачу под region-констрейнтом — следующая такая регрессия будет видна в логах, а не выглядеть как «DaData не знает этот адрес».

4. Инвалидация кэшаdata/sql/203_purge_geocode_cache_house_letter.sql. Точечный DELETE: (а) в запросе была литера, либо (б) запроса без литеры с закэшированным домом с литерой. Не весь кэш — полная очистка сожгла бы квоту DaData (10k/день) на ре-резолв заведомо корректных адресов. Идемпотентно (чистый DELETE по предикату, повторный прогон удалит 0 строк), безопасно для strict exit-1 авто-применения. Номер 203 — коллизий нет.

Проверено

SQL прогнан против живого реестра (v_tradein_cad_buildings, 47k строк), не только на моках:

запрос до после
новгородцевой 13б дом 13
новгородцевой 13 дом 13 дом 13
новгородцевой 23б д. 23-б (дефисная форма)
малышева 30 д. 30-б сооружение 30
малышева 30б д. 30-б
серова 27 д. 27 д. 27

Регрессия по охвату измерена: из 25 791 строки, проходивших старый prefilter, новую логику не проходят 20 — все вида «проезд 4-й ЕКАД Южный», т.е. достижимые ранее ТОЛЬКО через баг с д внутри «проезд». Реальной потери покрытия нет.

Предикат миграции прогнан против таблицы кейсов: 4 delete (обе стороны бага + |city=-суффикс ключа) / 8 keep (корректные записи, «8 марта 204», «1-я пятилетки 5», «58/3», «64-2», хвост-слово).

Тесты: переписан test_house_match_passes_only_digits_as_regex_param (он закреплял баг: assert params["house_digits"] == "26" для входа 26а) → теперь проверяет, что уходит полный house_norm == "26а". Добавлено +42 теста: извлечение токена из 20 реальных форм реестра, матрица match/no-match по обоим направлениям бага, регистр и разделители, фолбэк до Nominatim, константа DaData. Семантика SQL зеркалится на Python из той же константы (\m\b), поэтому правка регекспа автоматически меняет и проверки — рассинхрон невозможен.

uv run pytest (полный, tradein-mvp/backend): 1 failed, 3123 passed, 9 skipped.

Не проверено / честные оговорки

  • Падение test_search_api.py::test_search_cache_hit (401) — пре-существующее. Прогнал полный suite на чистой базе (стэш): baseline тоже 1 failed, 3081 passed. К этому изменению отношения не имеет. В изоляции этот файл вообще не собирается (ValidationError) — зависит от порядка в suite.
  • Ответ DaData с новым region-констрейнтом вживую не проверял — прод-токен из этой сессии недоступен (SSH заблокирован). Опираюсь на прогон против боевого API, сделанный на этапе диагностики: region='Свердловская область'0 (даже для «Малышева 30»), region='Свердловская'3, первый «г Екатеринбург, ул Новгородцевой, д 13б» (56.8332628, 60.6753297); region=None + city='Екатеринбург'5 включая «д 30Б»/«стр 30в». То есть токен валиден и DaData сам по себе работает — знает и отдаёт «13б»; ломал выдачу только формат значения region и на модель данных DaData (region без типа). Рассматривал альтернативу с kladr_id/region_iso_code — отверг: документацию по допустимым ключам locations подтвердить не удалось, а неверный ключ дал бы ровно тот же молчаливый ноль. Замер бьёт вывод из документации. Смоук после деплоя нужен.
  • Корпус не разбирается — «Новгородцевой 25 корп 2» по-прежнему парсится в дом «25» и может взять «д. 25, корп. 1». Пре-существующее поведение, отдельный класс (60 строк реестра), сознательно не трогал.
  • Запрос вида «13/2» падает в фолбэк_parse_street_house схлопывает его в «13», а «13» теперь строго не равно «13/2». Раньше результат был лотереей между «13» и «13/2»; фолбэк честнее. Чинить — значит править парсер, это за рамками.
  • Гео-фенс по Свердловской области не трогал; геопортал в suggest() не подключал; модуль не рефакторил.
## Проблема `_cadastral_house_match` сравнивал дом **только по цифрам**: литера была опциональна в `WHERE` (`[а-яё]?`) и участвовала лишь как tie-break в `ORDER BY`. Баг двусторонний и молчаливый: | запрос | возвращалось | реальность | |---|---|---| | «Новгородцевой 13б» | «дом 13» | дома 13б в реестре нет вообще | | «Малышева 30» | «д. 30-б» | живой прод-кейс, обратное направление | Оба результата шли с `confidence="exact"` и оседали в `geocode_cache` на 90 дней → пользователь получал оценку **чужого здания**, помеченную как точная. ## Что сделано **1. Строгое равенство номера дома.** Обе стороны приводятся к одному канону (`13б` / `13 б` / `13-б` / `13Б` → `13б`): запрос — существующим `_norm_house`, реестр — тем же выражением на стороне Postgres (`_SQL_HOUSE_TOKEN_RE` + `_SQL_HOUSE_TOKEN_NORM`). Дешёвый regex-anchor по цифрам оставлен **prefilter'ом** (пушится в FDW, режет «(1-83)»-диапазоны) — корректность теперь на равенстве, а не на нём. Формы литеры взяты из реальных данных, не выдуманы — замер по `gendesign_cad_buildings` (47k строк): | форма | строк | |---|---| | `д. 13` / `дом 13` / `сооружение 30` | 20 948 | | `д. 13б` | 2 646 | | `д. 13-б` | 2 093 | | `д. 58/3`, `д. 64-2` (угловые) | 922 | | `д. 13 б` | 125 | | `д. 102 корпус 1` | 60 | | `д. 11 (кв. 1-150)` / `(литера А)` | 49 (из них `литера` — **1**, проигнорирована) | Побочно закрыто той же логикой: - «58» больше не матчит «58/3», «13» не матчит «130» — угловой номер это часть номера дома; - маркер дома ищется только с начала слова (`\m`): раньше `д\.?` ловил «д» внутри «прое**з**д» и «проезд 8 Марта, д 5» давал дом «8»; - номер дома больше **не конкатенируется в regex** — regex-injection поверхность сузилась до цифр prefilter'а. **2. Промах ≠ тихая подстановка соседа.** Нет дома с нужной литерой → `None`, дальше отрабатывают следующие тиры. > ⚠️ **Исправлено 02.08.** В первой редакции здесь была указана цепочка `_cadastral_forward_sync` → DaData → Nominatim. **Тира DaData в `geocode()` нет** — `_dadata_suggest` вызывается ровно в одном месте, внутри `suggest()` (автокомплит). Реальная цепочка `geocode()`: cache → geoportal → cadastral → `_cadastral_forward_sync` → Nominatim → `None`. > > Следствие, принятое сознательно: на прямом вызове `geocode()` (API, PDF, восстановление по `?id=`) адрес с литерой, неизвестный ни геопорталу, ни Nominatim, вернёт `None` — оценка не построится, тогда как раньше строилась по соседнему дому. Честный отказ вместо уверенно-неверной оценки с меткой `exact`. > > **Основной UI-путь этим не задет:** координаты берутся из выбранной подсказки (`ParamsPanel.tsx:776` → `api/v1/trade_in.py:128` использует `lat`/`lon` напрямую), `geocode()` в этом сценарии не участвует. Докстринг `_cadastral_house_match` исправлен в `9f03733e`. *Почему None, а не пониженный confidence:* `GeocodeSuggestion` вообще не несёт `confidence` — его хардкодит `geocode()` (`"exact"` для всех трёх локальных тиров). Протаскивать новый канал уверенности через три тира ради этого случая — несоразмерно; фолбэк на внешние тиры и честнее, и дешевле. Проверено, что промах не утекает в соседний тир: `_cadastral_forward_sync` матчит **литеральной подстрокой** всего запроса, и «Новгородцевой 13б» не совпадает с «…Новгородцевой, дом 13» → 0 хитов → идём в DaData. **3. DaData-тир глушился фильтром региона.** В `locations` уходило `region="Свердловская область"`, тогда как DaData хранит имя региона **без типа** (`region="Свердловская"`, тип отдельно в `region_type="обл"`). `locations` — hard-filter, не boost: он не совпадал ни с чем и молча схлопывал выдачу в **0 подсказок** без всякой ошибки. Именно поэтому фолбэк для «13б» не спасал. Добавлен `logger.warning` на пустую выдачу под region-констрейнтом — следующая такая регрессия будет видна в логах, а не выглядеть как «DaData не знает этот адрес». **4. Инвалидация кэша** — `data/sql/203_purge_geocode_cache_house_letter.sql`. Точечный DELETE: (а) в запросе была литера, либо (б) запроса без литеры с закэшированным домом **с** литерой. Не весь кэш — полная очистка сожгла бы квоту DaData (10k/день) на ре-резолв заведомо корректных адресов. Идемпотентно (чистый DELETE по предикату, повторный прогон удалит 0 строк), безопасно для strict exit-1 авто-применения. Номер 203 — коллизий нет. ## Проверено **SQL прогнан против живого реестра** (`v_tradein_cad_buildings`, 47k строк), не только на моках: | запрос | до | после | |---|---|---| | новгородцевой 13б | дом 13 ❌ | ∅ ✅ | | новгородцевой 13 | дом 13 | дом 13 ✅ | | новгородцевой 23б | — | д. 23-б ✅ (дефисная форма) | | малышева 30 | д. 30-б ❌ | сооружение 30 ✅ | | малышева 30б | — | д. 30-б ✅ | | серова 27 | д. 27 | д. 27 ✅ | **Регрессия по охвату измерена:** из 25 791 строки, проходивших старый prefilter, новую логику не проходят **20** — все вида «проезд 4-й ЕКАД Южный», т.е. достижимые ранее ТОЛЬКО через баг с `д` внутри «проезд». Реальной потери покрытия нет. **Предикат миграции** прогнан против таблицы кейсов: 4 delete (обе стороны бага + `|city=`-суффикс ключа) / 8 keep (корректные записи, «8 марта 204», «1-я пятилетки 5», «58/3», «64-2», хвост-слово). **Тесты:** переписан `test_house_match_passes_only_digits_as_regex_param` (он закреплял баг: `assert params["house_digits"] == "26"` для входа `26а`) → теперь проверяет, что уходит полный `house_norm == "26а"`. Добавлено +42 теста: извлечение токена из 20 реальных форм реестра, матрица match/no-match по обоим направлениям бага, регистр и разделители, фолбэк до Nominatim, константа DaData. Семантика SQL зеркалится на Python **из той же константы** (`\m` → `\b`), поэтому правка регекспа автоматически меняет и проверки — рассинхрон невозможен. `uv run pytest` (полный, `tradein-mvp/backend`): **1 failed, 3123 passed, 9 skipped**. ## Не проверено / честные оговорки - **Падение `test_search_api.py::test_search_cache_hit` (401) — пре-существующее.** Прогнал полный suite на чистой базе (стэш): baseline тоже `1 failed, 3081 passed`. К этому изменению отношения не имеет. В изоляции этот файл вообще не собирается (ValidationError) — зависит от порядка в suite. - **Ответ DaData с новым region-констрейнтом вживую не проверял** — прод-токен из этой сессии недоступен (SSH заблокирован). Опираюсь на прогон против боевого API, сделанный на этапе диагностики: `region='Свердловская область'` → **0** (даже для «Малышева 30»), `region='Свердловская'` → **3**, первый «г Екатеринбург, ул Новгородцевой, д 13б» (56.8332628, 60.6753297); `region=None` + `city='Екатеринбург'` → **5** включая «д 30Б»/«стр 30в». То есть токен валиден и DaData сам по себе работает — знает и отдаёт «13б»; ломал выдачу только формат значения `region` и на модель данных DaData (`region` без типа). Рассматривал альтернативу с `kladr_id`/`region_iso_code` — отверг: документацию по допустимым ключам `locations` подтвердить не удалось, а неверный ключ дал бы ровно тот же молчаливый ноль. Замер бьёт вывод из документации. **Смоук после деплоя нужен.** - **Корпус не разбирается** — «Новгородцевой 25 корп 2» по-прежнему парсится в дом «25» и может взять «д. 25, корп. 1». Пре-существующее поведение, отдельный класс (60 строк реестра), сознательно не трогал. - **Запрос вида «13/2» падает в фолбэк** — `_parse_street_house` схлопывает его в «13», а «13» теперь строго не равно «13/2». Раньше результат был лотереей между «13» и «13/2»; фолбэк честнее. Чинить — значит править парсер, это за рамками. - Гео-фенс по Свердловской области не трогал; геопортал в `suggest()` не подключал; модуль не рефакторил.
lekss361 added 1 commit 2026-08-02 10:31:56 +00:00
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
d63702cfec
`_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.
Author
Owner

Поправка к описанию PR — не мержить, пока не принято решение

Проверено главной сессией по коду ветки d63702cf, ревью подтверждено:

В описании PR и в докстринге _cadastral_house_match заявлена цепочка фолбэка «промах → forward-ILIKE → DaData → Nominatim». В geocode() тира DaData нет.

$ git grep -n "_dadata_suggest" -- app/services/geocoder.py
641:  async def _dadata_suggest(...)          # определение
1227: dadata_results = await _dadata_suggest(query, limit)   # ← единственный вызов, внутри suggest()

Реальная цепочка geocode(): cache → geoportal → cadastral → forward-ILIKE → Nominatim → return None.
_dadata_suggest живёт только в suggest() — это автокомплит, отдельный путь.

Что это меняет по факту

На прямом вызове geocode() (API, PDF, восстановление по ?id= — всё, что минует выбор из автокомплита) адрес с литерой, который не вытянули ни геопортал, ни Nominatim, теперь вернёт Noneоценка не построится вообще, тогда как раньше строилась по соседнему дому.

Это защитимое поведение: молчаливая неверная оценка с меткой confidence="exact" хуже честного отказа, и это прямо соответствует honesty-gates из стратегического аудита. Но это продуктовое решение, а не техническая деталь, и обосновывать мерж страховкой, которой не существует, нельзя.

Собственный новый тест автора test_geocode_letter_house_miss_reaches_nominatim мокает именно реальную цепочку и заканчивается на Nominatim — то есть тест доказывает обратное тому, что написано в описании.

Что нужно до мержа

  1. Поправить описание PR и докстринг _cadastral_house_match:1019-1020 — убрать DaData из заявленной цепочки geocode().
  2. Явно принять поведение «нет точного дома с литерой → отказ вместо оценки соседнего» как продуктовое решение владельца.
  3. Смоук DaData после деплоя — тир suggest() до этого PR возвращал 0 подсказок на любой запрос и вживую после правки не проверялся (прод-токен был недоступен исполнителю). До смоука тир считать непроверенным, а не исправленным.

Ядро фикса (строгое сравнение нормализованных форм номера дома вместо «цифры совпали, литера опциональна») ревью подтвердило прогоном SQL против живого реестра: 13б→∅, 13→дом 13, 30→сооружение 30 вместо «30-б», 23б→«д. 23-б». Регрессия покрытия измерена: из 25 791 строки старого prefilter'а новую логику не проходят 20, все — «проезд N-й», достижимые ранее только через сам баг.

## Поправка к описанию PR — не мержить, пока не принято решение Проверено главной сессией по коду ветки `d63702cf`, ревью подтверждено: **В описании PR и в докстринге `_cadastral_house_match` заявлена цепочка фолбэка «промах → forward-ILIKE → DaData → Nominatim». В `geocode()` тира DaData нет.** ``` $ git grep -n "_dadata_suggest" -- app/services/geocoder.py 641: async def _dadata_suggest(...) # определение 1227: dadata_results = await _dadata_suggest(query, limit) # ← единственный вызов, внутри suggest() ``` Реальная цепочка `geocode()`: cache → geoportal → cadastral → forward-ILIKE → Nominatim → `return None`. `_dadata_suggest` живёт только в `suggest()` — это автокомплит, отдельный путь. ### Что это меняет по факту На прямом вызове `geocode()` (API, PDF, восстановление по `?id=` — всё, что минует выбор из автокомплита) адрес с литерой, который не вытянули ни геопортал, ни Nominatim, теперь вернёт `None` → **оценка не построится вообще**, тогда как раньше строилась по соседнему дому. Это защитимое поведение: молчаливая неверная оценка с меткой `confidence="exact"` хуже честного отказа, и это прямо соответствует honesty-gates из стратегического аудита. **Но это продуктовое решение, а не техническая деталь**, и обосновывать мерж страховкой, которой не существует, нельзя. Собственный новый тест автора `test_geocode_letter_house_miss_reaches_nominatim` мокает именно реальную цепочку и заканчивается на Nominatim — то есть тест доказывает обратное тому, что написано в описании. ### Что нужно до мержа 1. Поправить описание PR и докстринг `_cadastral_house_match:1019-1020` — убрать DaData из заявленной цепочки `geocode()`. 2. Явно принять поведение «нет точного дома с литерой → отказ вместо оценки соседнего» как продуктовое решение владельца. 3. Смоук DaData после деплоя — тир `suggest()` до этого PR возвращал 0 подсказок на любой запрос и вживую после правки не проверялся (прод-токен был недоступен исполнителю). До смоука тир считать непроверенным, а не исправленным. Ядро фикса (строгое сравнение нормализованных форм номера дома вместо «цифры совпали, литера опциональна») ревью подтвердило прогоном SQL против живого реестра: `13б`→∅, `13`→дом 13, `30`→сооружение 30 вместо «30-б», `23б`→«д. 23-б». Регрессия покрытия измерена: из 25 791 строки старого prefilter'а новую логику не проходят 20, все — «проезд N-й», достижимые ранее только через сам баг.
bot-backend added 1 commit 2026-08-02 11:56:38 +00:00
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
9f03733e46
Докстринг _cadastral_house_match утверждал, что промах уходит в цепочку
_cadastral_forward_sync → DaData → Nominatim. Тира DaData в geocode() нет:
_dadata_suggest вызывается ровно в одном месте — внутри suggest() (автокомплит).

Реальные цепочки разведены явно, плюс зафиксировано следствие: на прямом
вызове geocode() адрес с литерой, неизвестный геопорталу и Nominatim, даёт
None вместо оценки соседнего дома. Основной UI-путь не задет — координаты
приходят из выбранной подсказки и минуют geocode().
Author
Owner

Решение владельца принято — мержим

Поведение «нет точного дома с литерой → отказ вместо оценки соседнего» утверждено владельцем 02.08.2026 как продуктовое решение.

Область действия узкая: прямой вызов geocode() без координат — API, PDF, восстановление по ?id=. Основной UI-сценарий не затронут: координаты приходят из выбранной подсказки (ParamsPanel.tsx:776trade_in.py:128 использует lat/lon напрямую), geocode() в нём не участвует.

Обоснование: уверенно-неверная оценка чужого здания с меткой confidence="exact", оседающая в geocode_cache на 90 дней, хуже честного отказа. Соответствует honesty-gates стратегического аудита.

Pre-merge: все 7 проверок зелёные на 9f03733e, включая явный CI Trade-In / backend-tests и CI / backend-tests.

Post-deploy обязательно (в PR остаётся непроверенным):

  1. Автокомплит: ввести «Новгородцевой 13» → «13б» должен появиться в подсказках. Это первый живой прогон DaData после смены значения region.
  2. Проверить, что миграция 203_purge_geocode_cache_house_letter.sql применилась и почистила только целевые записи кэша.

Второй PR (#2621, атрибуция Leaflet) мержится отдельно, после завершения деплоя этого — оба триггерят deploy-tradein.yml, а два мержа подряд роняют test-job и дают «успешный деплой» на старом образе.

## Решение владельца принято — мержим Поведение **«нет точного дома с литерой → отказ вместо оценки соседнего»** утверждено владельцем 02.08.2026 как продуктовое решение. Область действия узкая: прямой вызов `geocode()` без координат — API, PDF, восстановление по `?id=`. Основной UI-сценарий не затронут: координаты приходят из выбранной подсказки (`ParamsPanel.tsx:776` → `trade_in.py:128` использует `lat`/`lon` напрямую), `geocode()` в нём не участвует. Обоснование: уверенно-неверная оценка чужого здания с меткой `confidence="exact"`, оседающая в `geocode_cache` на 90 дней, хуже честного отказа. Соответствует honesty-gates стратегического аудита. **Pre-merge:** все 7 проверок зелёные на `9f03733e`, включая явный `CI Trade-In / backend-tests` и `CI / backend-tests`. **Post-deploy обязательно** (в PR остаётся непроверенным): 1. Автокомплит: ввести «Новгородцевой 13» → «13б» должен появиться в подсказках. Это первый живой прогон DaData после смены значения `region`. 2. Проверить, что миграция `203_purge_geocode_cache_house_letter.sql` применилась и почистила только целевые записи кэша. Второй PR (#2621, атрибуция Leaflet) мержится **отдельно, после завершения деплоя этого** — оба триггерят `deploy-tradein.yml`, а два мержа подряд роняют `test`-job и дают «успешный деплой» на старом образе.
lekss361 merged commit 0abfe020df into main 2026-08-02 12:06:45 +00:00
lekss361 deleted branch fix/tradein-geocoder-house-letter 2026-08-02 12:06:45 +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#2622
No description provided.