fix(tradein/matching): честность тиров сопоставления домов (#2674) #2688

Merged
bot-backend merged 2 commits from fix/2674-matching-tiers-honesty into main 2026-08-06 00:18:07 +00:00
Collaborator

Ревизия после ревью. Первая версия PR содержала слияние 781 дублирующего дома по расширенному ключу дедупа. Оно снято полностью — обоснование было круговым, разбор ниже в разделе «Снятое». Остались находки эпика и три правки, которые ревью попросило добавить.

Все числа — read-only с прод tradein-postgres, 2026-08-05/06.

Разбор трёх находок #2674

1. Кадастровый номер пуст у всех объявлений → фильтр поиска снят

listings.cadastral_number (кадастр квартиры) — 0 из 93 408. Единственный писатель, парсер Циана (providers/cian/serp.py:872), читает offer["cadastralNumber"] — ключа в ответе нет. Ни одна другая площадка кадастр даже не парсит.

building_cadastral_number заполнено 28 504 (30.5%) — и все 28 504 на 100% пришли из локального гео-зеркала ЕГРН, а не от площадок (джойн к cad_buildings_local, вне зеркала ноль).

источник объявлений cadastral_number building_cadastral_number
avito 47 971 0 13 426
cian 21 740 0 7 599
yandex 16 721 0 3 787
domklik 6 594 0 3 566
n1 382 0 126

Решение: снят фильтр has_kadastr (services/search_query.py, schemas/search.py). Предикат cadastral_number IS NOT NULL мог вернуть только пустую выдачу — обещание качества данных, которого нет. Колонка и парсер оставлены: писатель рабочий, источник пуст. Фронтенд параметр не шлёт, лишний query-param FastAPI игнорирует.

2. Тир по кадастру: тир оставлен, приёмник сужен

«Недостижим по построению» подтвердилось наполовину: путь скрейпинга кадастр в матчер передаёт, тир мёртв из-за отсутствия данных, а не структуры.

Дешёвый фикс «подать накопленные 28 504 гео-кадастра» отвергнут числами: KNN-заполнение (ближайшее здание ≤50 м) не инъективно ни в одну сторону — 656 из 3 260 значений накрывают >1 здание ГАР (20.1%), 751 из 2 864 зданий получают >1 значение (26.2%). Тир с confidence = 1.0 на таком ключе — over-merge на пятой части корпуса.

Но «оставленный рабочий приёмник» сам оказался отложенной миной, и она обезврежена. Приёмник не различал два кадастра: cad = building_cadastral_number or cadastral_number. Начни Циан отдавать offer["cadastralNumber"] — это кадастр квартиры, у каждой свой. Tier 0 не сматчил бы никогда → New-house INSERT → номер квартиры в houses.cadastral_number, и попутно снят P1-страж «безномерный адрес без кадастра не создаём» (cad там же разрешает создание). Две квартиры одного дома → два дома, то самое дробление.

Параметр cadastral_number убран из match_or_create_house, Protocol HouseMatcher, RealMatcherAdapter и обоих вызывающих (scraper_kit/base.py, scripts/backfill_listing_sources.py). В listings оба поля пишутся как раньше — из ключа дома ушло только ложное.

Заодно исправлено ложное утверждение в шапке tasks/cadastral_geo_match.py, будто «Tier-0 трактует building_cadastral_number как подсказку» — Tier 0 отдаёт 1.0.

3. Тир по ФИАС: удалён в пути создания, оставлен в read-only

  • match_or_create_house(house_fias_id=…) — параметра нет ни в Protocol, ни в адаптере, ни у двух прямых вызывающих. Передать было некому. Удалён.
  • match_house_readonly(house_fias_id=…) — источник реальный: estimator.py:3436 передаёт payload.target_fias_id or dadata.house_fias_id. Оставлен, замер подтверждает срабатывания.

Подключать ФИАС в скрейпинг не стали: у скрейпера только адрес, резолв = DaData на каждое объявление в горячем пути.


Оценка: насколько хуже стало сопоставление

Верхние тиры — ноль за всю историю. house_sources, 49 502 строки: fingerprint 58.97%, new 22.65%, geo_proximity 18.36%, cadastr_exact 0, fias_exact 0. 100% домов сматчено слабее.

Дробление измерено: 653 кластера / 1 434 дома / 781 лишняя строка (8.3% таблицы houses), 6 389 объявлений на дублях, худший случай 6 записей на здание. Виновники: fingerprint 2 550, geo_proximity 1 560, new 1 294. Это #1772 и прямое следствие мёртвых верхних тиров.

Склейку измерить не удалось — единственный доступный ключ (KNN-кадастр) сам даёт 20% ложных совпадений.

Схлопывание этих 781 в PR не входит (см. ниже). Замер остаётся в силе как оценка ущерба.


Снятое: слияние по COALESCE(house_fias_id, gar_house_guid)

Первая версия расширяла ключ дедупа, считая gar_house_guid независимым наблюдением идентичности. Он им не является, ревью право. gar_flats_loader._MATCH_SQL:

WHERE tradein_canon_addr(COALESCE(h.short_address, h.full_address, h.address)) = gp.canon

Левая часть побайтово равна ключу канон-прохода (_CANON_KEY_EXPR). Значит guid — детерминированная функция канон-адреса, а не второе наблюдение: два дома получают один guid тогда и только тогда, когда у них один канон.

А проход по этому ключу идёт с выключенным гео-стражем («идентичность старше близости»). Для 764 пар из 781 это круговой аргумент: идентичность здесь и есть адрес. Канон-проход отказывается слить два дома в 6 км — этот сливал их же за «общий UUID», выданный за тот же адрес. Защита #2187 обходилась боковой дверью.

Сверх того: gar_pick берёт DISTINCT ON (canon) — одна ГАР-строка на канон, а 20.3% канонов накрывают несколько зданий (советская20 → 34), и ЕКБ-фильтр стоит только на стороне ГАР. Кедровка/Советская 17 уехала бы в ЕКБ за 16 км. Страж расхождения идентификаторов сравнивает только старое поле и на этих 781 не вычисляется ни разу — защиты не было вообще.

Задача не в доработке, а в другом замысле: нужен ключ, независимый от канон-адреса, либо гео-страж, включённый и для этого прохода. Отдельным issue.

Добавлено по ревью

listing_cnt DESC NULLS LAST (house_dedup_merge). Счётчик приходит из LEFT JOIN listing_counts → у дома без объявлений он NULL, а DESC в Postgres — NULLS FIRST, то есть пустая запись обгоняла запись со 192 объявлениями вопреки задокументированному правилу «most linked listings». Дефект предсуществующий и живой для канон-прохода, который пары даёт; последствие не косметическое — объявления переезжают на запись, на которую корпус никогда не ссылался, а COALESCE-перенос полей неполон.

Сторож границы вызова для живого ФИАС-тира. Прежние проверки структурные — видят имя параметра в сигнатуре. Уберут аргумент на настоящей границе (estimator.estimate_qualitymatch_house_readonly) — сигнатура цела, тесты зелёные, тир снова мёртв. Новый тест смотрит на сам вызов.

Снято преувеличение. Формулировка «тест ловит класс, который эпик считает неуловимым» убрана: структурная проверка ловит подслучай и обходится двумя способами (объявить параметр везде и не пробросить в теле; не передать на настоящей границе). Доказательство рядом — building_cadastral_number эту проверку проходит при нуле срабатываний из 49 502.

Тесты

tests/test_matching_tier_reachability_2674.py — сверка сигнатуры матчера с границей вызова (Protocol + адаптер), guard на квартирный кадастр, guard на реальную передачу ФИАС.

Удалены два теста Tier 0.5 — они были зелёными ровно потому, что обходили границу вызова.

Фальсификация (патч-метод: откат app/+scripts/+packages/ при сохранённых тестах):

  • test_create_path_matcher_params_are_all_reachable_from_the_boundary → FAILED
  • test_fias_tier_is_gone_from_create_path_but_alive_in_readonly → FAILED
  • test_house_key_never_accepts_flat_cadastre → FAILED
  • test_keeper_listing_count_puts_nulls_last → FAILED
  • test_no_unsatisfiable_cadastral_filter → FAILED (отдельным прогоном, откат search_query.py)

Честно: test_readonly_fias_tier_is_actually_fed_by_its_caller при откате зелёный — он сторожит estimator.py, который PR не меняет. Это регрессионный сторож, а не проверка фикса; краснеть ему нечем, пока аргумент на месте.

Полный прогон: 3 507 passed, 9 skipped, 1 failed. Упавший — test_search_api.py::test_search_cache_hit (401 вместо 200), предсуществующий: падает и на неизменённом origin/main (проверено откатом файла).

Test plan

  • pytest tests/ -q — 3 507 passed, 1 предсуществующий fail
  • фальсификация патч-методом — 5 краснеют, 1 честно нет
  • пост-деплой: поиск отвечает 200 при has_kadastr в теле (параметр игнорируется)
  • пост-деплой: следующий прогон house_dedup_merge не даёт всплеска losers_deleted (ключ не менялся, изменился только порядок выбора keeper'а)

Что НЕ входит

  • Слияние 781 дубля — снято, нужен независимый ключ (отдельный issue).
  • Миграций нет; дроп listings.cadastral_number — для database-expert.
  • match_or_create_listing (matching/listings.py) мёртв целиком — ноль прод-вызывающих, отдельная находка.
  • Аудит слияний (кто в кого свернулся) отсутствует, логи ротируются меньше суток — предсуществующий пробел, отдельная задача.
  • Склейка разных зданий в одну запись не измерена.
  • Фронтенд не трогал.

Refs #2674

> **Ревизия после ревью.** Первая версия PR содержала слияние 781 дублирующего дома по расширенному ключу дедупа. Оно **снято полностью** — обоснование было круговым, разбор ниже в разделе «Снятое». Остались находки эпика и три правки, которые ревью попросило добавить. Все числа — read-only с прод `tradein-postgres`, 2026-08-05/06. ## Разбор трёх находок #2674 ### 1. Кадастровый номер пуст у всех объявлений → фильтр поиска снят `listings.cadastral_number` (кадастр **квартиры**) — **0 из 93 408**. Единственный писатель, парсер Циана (`providers/cian/serp.py:872`), читает `offer["cadastralNumber"]` — ключа в ответе нет. Ни одна другая площадка кадастр даже не парсит. `building_cadastral_number` заполнено 28 504 (30.5%) — и **все 28 504 на 100% пришли из локального гео-зеркала ЕГРН**, а не от площадок (джойн к `cad_buildings_local`, вне зеркала ноль). | источник | объявлений | cadastral_number | building_cadastral_number | |---|---|---|---| | avito | 47 971 | 0 | 13 426 | | cian | 21 740 | 0 | 7 599 | | yandex | 16 721 | 0 | 3 787 | | domklik | 6 594 | 0 | 3 566 | | n1 | 382 | 0 | 126 | **Решение:** снят фильтр `has_kadastr` (`services/search_query.py`, `schemas/search.py`). Предикат `cadastral_number IS NOT NULL` мог вернуть только пустую выдачу — обещание качества данных, которого нет. Колонка и парсер оставлены: писатель рабочий, источник пуст. Фронтенд параметр не шлёт, лишний query-param FastAPI игнорирует. ### 2. Тир по кадастру: тир оставлен, приёмник сужен «Недостижим по построению» подтвердилось наполовину: путь скрейпинга кадастр в матчер *передаёт*, тир мёртв из-за отсутствия данных, а не структуры. Дешёвый фикс «подать накопленные 28 504 гео-кадастра» **отвергнут числами**: KNN-заполнение (ближайшее здание ≤50 м) не инъективно ни в одну сторону — 656 из 3 260 значений накрывают >1 здание ГАР (**20.1%**), 751 из 2 864 зданий получают >1 значение (**26.2%**). Тир с `confidence = 1.0` на таком ключе — over-merge на пятой части корпуса. **Но «оставленный рабочий приёмник» сам оказался отложенной миной, и она обезврежена.** Приёмник не различал два кадастра: `cad = building_cadastral_number or cadastral_number`. Начни Циан отдавать `offer["cadastralNumber"]` — это кадастр **квартиры**, у каждой свой. Tier 0 не сматчил бы никогда → New-house INSERT → номер квартиры в `houses.cadastral_number`, и попутно снят P1-страж «безномерный адрес без кадастра не создаём» (`cad` там же разрешает создание). Две квартиры одного дома → два дома, то самое дробление. Параметр `cadastral_number` убран из `match_or_create_house`, Protocol `HouseMatcher`, `RealMatcherAdapter` и обоих вызывающих (`scraper_kit/base.py`, `scripts/backfill_listing_sources.py`). В `listings` оба поля пишутся как раньше — из ключа дома ушло только ложное. Заодно исправлено ложное утверждение в шапке `tasks/cadastral_geo_match.py`, будто «Tier-0 трактует building_cadastral_number как подсказку» — Tier 0 отдаёт 1.0. ### 3. Тир по ФИАС: удалён в пути создания, оставлен в read-only * `match_or_create_house(house_fias_id=…)` — параметра нет ни в Protocol, ни в адаптере, ни у двух прямых вызывающих. Передать было **некому**. **Удалён.** * `match_house_readonly(house_fias_id=…)` — источник реальный: `estimator.py:3436` передаёт `payload.target_fias_id or dadata.house_fias_id`. **Оставлен**, замер подтверждает срабатывания. Подключать ФИАС в скрейпинг не стали: у скрейпера только адрес, резолв = DaData на каждое объявление в горячем пути. --- ## Оценка: насколько хуже стало сопоставление **Верхние тиры — ноль за всю историю.** `house_sources`, 49 502 строки: fingerprint 58.97%, new 22.65%, geo_proximity 18.36%, `cadastr_exact` **0**, `fias_exact` **0**. 100% домов сматчено слабее. **Дробление измерено:** 653 кластера / 1 434 дома / **781 лишняя строка (8.3% таблицы `houses`)**, 6 389 объявлений на дублях, худший случай 6 записей на здание. Виновники: fingerprint 2 550, geo_proximity 1 560, new 1 294. Это #1772 и прямое следствие мёртвых верхних тиров. **Склейку измерить не удалось** — единственный доступный ключ (KNN-кадастр) сам даёт 20% ложных совпадений. **Схлопывание этих 781 в PR не входит** (см. ниже). Замер остаётся в силе как оценка ущерба. --- ## Снятое: слияние по `COALESCE(house_fias_id, gar_house_guid)` Первая версия расширяла ключ дедупа, считая `gar_house_guid` независимым наблюдением идентичности. **Он им не является, ревью право.** `gar_flats_loader._MATCH_SQL`: ```sql WHERE tradein_canon_addr(COALESCE(h.short_address, h.full_address, h.address)) = gp.canon ``` Левая часть **побайтово равна** ключу канон-прохода (`_CANON_KEY_EXPR`). Значит guid — детерминированная функция канон-адреса, а не второе наблюдение: два дома получают один guid **тогда и только тогда**, когда у них один канон. А проход по этому ключу идёт с **выключенным гео-стражем** («идентичность старше близости»). Для 764 пар из 781 это круговой аргумент: идентичность здесь и есть адрес. Канон-проход отказывается слить два дома в 6 км — этот сливал их же за «общий UUID», выданный за тот же адрес. **Защита #2187 обходилась боковой дверью.** Сверх того: `gar_pick` берёт `DISTINCT ON (canon)` — одна ГАР-строка на канон, а 20.3% канонов накрывают несколько зданий (`советская20` → 34), и ЕКБ-фильтр стоит **только на стороне ГАР**. Кедровка/Советская 17 уехала бы в ЕКБ за 16 км. Страж расхождения идентификаторов сравнивает только старое поле и на этих 781 не вычисляется ни разу — защиты не было вообще. Задача не в доработке, а в другом замысле: нужен ключ, независимый от канон-адреса, либо гео-страж, включённый и для этого прохода. Отдельным issue. ## Добавлено по ревью **`listing_cnt DESC NULLS LAST`** (`house_dedup_merge`). Счётчик приходит из `LEFT JOIN listing_counts` → у дома без объявлений он NULL, а `DESC` в Postgres — NULLS FIRST, то есть пустая запись обгоняла запись со 192 объявлениями вопреки задокументированному правилу «most linked listings». Дефект предсуществующий и **живой для канон-прохода**, который пары даёт; последствие не косметическое — объявления переезжают на запись, на которую корпус никогда не ссылался, а COALESCE-перенос полей неполон. **Сторож границы вызова для живого ФИАС-тира.** Прежние проверки структурные — видят имя параметра в сигнатуре. Уберут аргумент на настоящей границе (`estimator.estimate_quality` → `match_house_readonly`) — сигнатура цела, тесты зелёные, тир снова мёртв. Новый тест смотрит на сам вызов. **Снято преувеличение.** Формулировка «тест ловит класс, который эпик считает неуловимым» убрана: структурная проверка ловит подслучай и обходится двумя способами (объявить параметр везде и не пробросить в теле; не передать на настоящей границе). Доказательство рядом — `building_cadastral_number` эту проверку проходит при нуле срабатываний из 49 502. ## Тесты `tests/test_matching_tier_reachability_2674.py` — сверка сигнатуры матчера с границей вызова (Protocol + адаптер), guard на квартирный кадастр, guard на реальную передачу ФИАС. Удалены два теста Tier 0.5 — они были зелёными ровно потому, что обходили границу вызова. **Фальсификация (патч-метод: откат `app/`+`scripts/`+`packages/` при сохранённых тестах):** * `test_create_path_matcher_params_are_all_reachable_from_the_boundary` → FAILED * `test_fias_tier_is_gone_from_create_path_but_alive_in_readonly` → FAILED * `test_house_key_never_accepts_flat_cadastre` → FAILED * `test_keeper_listing_count_puts_nulls_last` → FAILED * `test_no_unsatisfiable_cadastral_filter` → FAILED (отдельным прогоном, откат `search_query.py`) **Честно:** `test_readonly_fias_tier_is_actually_fed_by_its_caller` при откате **зелёный** — он сторожит `estimator.py`, который PR не меняет. Это регрессионный сторож, а не проверка фикса; краснеть ему нечем, пока аргумент на месте. Полный прогон: **3 507 passed, 9 skipped, 1 failed**. Упавший — `test_search_api.py::test_search_cache_hit` (401 вместо 200), **предсуществующий**: падает и на неизменённом `origin/main` (проверено откатом файла). ## Test plan - [x] `pytest tests/ -q` — 3 507 passed, 1 предсуществующий fail - [x] фальсификация патч-методом — 5 краснеют, 1 честно нет - [ ] пост-деплой: поиск отвечает 200 при `has_kadastr` в теле (параметр игнорируется) - [ ] пост-деплой: следующий прогон `house_dedup_merge` не даёт всплеска `losers_deleted` (ключ не менялся, изменился только порядок выбора keeper'а) ## Что НЕ входит * Слияние 781 дубля — снято, нужен независимый ключ (отдельный issue). * Миграций нет; дроп `listings.cadastral_number` — для database-expert. * `match_or_create_listing` (`matching/listings.py`) мёртв целиком — ноль прод-вызывающих, отдельная находка. * Аудит слияний (кто в кого свернулся) отсутствует, логи ротируются меньше суток — предсуществующий пробел, отдельная задача. * Склейка разных зданий в одну запись не измерена. * Фронтенд не трогал. Refs #2674
bot-backend added 1 commit 2026-08-05 23:40:00 +00:00
fix(tradein/matching): честность тиров сопоставления домов (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 7s
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 2m54s
3fd6550a16
Три находки эпика #2674 про верхние тиры матчинга домов. Замеры — прод
tradein-postgres, 2026-08-05/06.

Кадастр от площадок не приходит вообще. listings.cadastral_number (кадастр
КВАРТИРЫ) — 0 из 93 408; единственный писатель, парсер Циана, читает
offer["cadastralNumber"], которого в ответе нет. Все 28 504 заполненных
building_cadastral_number на 100% пришли из локального гео-зеркала ЕГРН
(tasks/cadastral_geo_match.py, KNN <=50 м) — проверено джойном к
cad_buildings_local. Поэтому снят фильтр поиска has_kadastr: предикат
`cadastral_number IS NOT NULL` мог вернуть только пустую выдачу. Колонка и
писатель оставлены — заработают сами, если площадка начнёт отдавать кадастр.

Tier 0 cadastr_exact оставлен, но не подключён к гео-кадастру. Он достижим по
построению (ScrapedLot -> адаптер -> матчер), просто данных нет; подать туда
KNN-заполнение НЕЛЬЗЯ: как ключ здания оно не инъективно — 656 из 3 260
значений накрывают >1 здание ГАР (20.1%), 751 из 2 864 зданий получают >1
значение (26.2%). Это был бы over-merge с confidence 1.0. Заодно исправлено
ложное утверждение в шапке cadastral_geo_match.py, будто Tier 0 трактует эту
колонку как подсказку.

Tier 0.5 fias_exact удалён из match_or_create_house. Параметра house_fias_id
не было ни в Protocol scraper_kit.contracts.HouseMatcher, ни в
RealMatcherAdapter, ни у двух прямых вызывающих — передать его было некому.
В match_house_readonly тир оставлен: у estimate-пути источник ФИАС есть
(payload.target_fias_id / DaData).

Что чинит сопоставление на самом деле: ключ идентичности в house_dedup_merge
расширен с house_fias_id до COALESCE(house_fias_id, gar_house_guid). Это один
и тот же UUID здания в ГАР (3 666 совпадений из 3 667 домов, где заполнены оба),
но заполняют его разные источники, и половина в проход не входила. Read-only
прогон отрендеренного mapping-SQL на проде: старый ключ — 0 пар, новый — 781
(8.3% таблицы houses, 6 389 объявлений на них). Канон-проход эти дома узнаёт
(900 пар из 919 имеют один канон-адрес), но блокирует гео-стражем: 356 пар
с NULL geom, 457 дальше 250 м (максимум 5 065 км — битый геокод). Ровно
аргумент #2187: общий UUID здания старше близости.

Качество сопоставления сейчас: 0 из 49 502 строк house_sources сматчены
верхними тирами; fingerprint 58.97%, new 22.65%, geo_proximity 18.36%.

Тесты: новый tests/test_matching_tier_reachability_2674.py сверяет параметры
матчера с границей вызова (Protocol + адаптер) — ловит класс «ветка есть,
передать некому», который обычный тест не видит, потому что зовёт функцию
напрямую. Удалены два теста мёртвого fias-тира: они были зелёными ровно
потому, что обходили границу вызова.

Refs #2674
Light1YT added 1 commit 2026-08-06 00:13:29 +00:00
fix(tradein/matching): снять слияние по ГАР-GUID, починить приёмник кадастра и keeper (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m54s
1a577fe748
Ревью PR #2688 нашло, что расширение ключа дедупа было неверным. Снимаю его
полностью и добавляю три правки, которых не хватало.

СНЯТО: слияние 781 дома по COALESCE(house_fias_id, gar_house_guid).
Аргумент «общий UUID здания есть независимая идентичность» оказался круговым.
gar_flats_loader проставляет gar_house_guid предикатом
  WHERE tradein_canon_addr(COALESCE(h.short_address, h.full_address, h.address)) = gp.canon
— левая часть побайтово равна ключу канон-прохода, то есть guid является
детерминированной функцией канон-адреса, а не вторым наблюдением. Проход шёл
с выключенным гео-стражем, значит #2187 обходился боковой дверью: канон-проход
отказывается слить два дома в 6 км, а этот сливал их же за «общий UUID»,
выданный за тот же адрес. Плюс gar_pick берёт DISTINCT ON (canon) — одна
ГАР-строка на канон, а 20.3% канонов накрывают несколько зданий, и ЕКБ-фильтр
стоит только на стороне ГАР. Кедровка/Советская 17 уехала бы в ЕКБ. Нужен
ключ, независимый от канона, либо включённый гео-страж — это другая задача.

Приёмник кадастра сужен до кадастра ЗДАНИЯ. Параметр cadastral_number (кадастр
КВАРТИРЫ) убран из match_or_create_house, Protocol HouseMatcher,
RealMatcherAdapter и обоих вызывающих; `cad` больше не падает на него фолбэком.
Мина была отложенной: начни Циан отдавать offer["cadastralNumber"], который
парсер уже читает, — у каждой квартиры свой номер, Tier 0 не сматчил бы
никогда, New-house INSERT записал бы номер квартиры в houses.cadastral_number
и попутно снял P1-страж «безномерный адрес без кадастра не создаём». Две
квартиры одного дома дали бы два дома — то самое дробление. В listings оба
поля пишутся как раньше.

Keeper: listing_cnt DESC NULLS LAST. Счётчик приходит из LEFT JOIN, у дома без
объявлений он NULL, а DESC в Postgres — NULLS FIRST, поэтому пустая запись
обгоняла запись со 192 объявлениями вопреки задокументированному правилу.
Дефект предсуществующий и живой для канон-прохода.

Сторож границы вызова для живого ФИАС-тира. Прежние проверки были
структурными — видели имя параметра в сигнатуре. Уберут аргумент на настоящей
границе (estimator.estimate_quality -> match_house_readonly) — сигнатура цела,
тесты зелёные, тир снова мёртв. Новый тест смотрит на сам вызов. Заявление
«тест ловит неуловимый класс» из прошлого описания снято как преувеличение:
структурная проверка ловит подслучай, и building_cadastral_number её проходит
при нуле срабатываний из 49 502.

Остаётся из первого захода: снятый фильтр поиска has_kadastr, разделение
ФИАС-тира (удалён в пути создания, оставлен в read-only), поправка ложного
утверждения в шапке cadastral_geo_match.py.

Refs #2674
bot-backend merged commit f76485781b into main 2026-08-06 00:18:07 +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#2688
No description provided.