fix(tradein/matching): город развёртки доезжает до стража Tier-2a — межгородские склейки домов (#2777) #2808

Merged
bot-backend merged 1 commit from fix/2777-cross-city-matching into main 2026-08-10 08:51:15 +00:00
Collaborator

Что чинится

Бескоординатная карточка областного формата — «ул. Кирова,4», города в адресе НЕТ — садилась в одноимённый дом другого города. Ключ Tier-2a address_fingerprint(address, lat, lon) без координат вырождается в один нормализованный адрес и становится глобально уникальным в house_address_aliases.

Страж от ровно этой коллизии в матчере был (_skip_oblast_alias), но выводил город из текста адреса — а областной Avito SERP города не называет. Комментарий в коде это и описывал: «a BARE oblast card … carries no signal here … that residual needs sweep-context».

Sweep-context был собран и не доезжал. save_listings знает город-цель batch'а, прогоняет его через собственный гео-guard (city_anchor/city_radius_km) и пишет в listings.city. А _link_listing_to_house города не получал, и у match_or_create_house такого параметра не было. Признак лежал в соседней колонке той же строки.

Город здесь — независимое наблюдение (какой город запрашивала развёртка), не второе поле из той же строки адреса. Именно на этом различии в #2690 сорвалось усиление ключа через gar_house_guid.

Перемеренные числа (прод, 2026-08-10, после схлопывания #2704)

замер issue (08-07) сегодня
домов без координат 1945 706
коорд-less домов с разбросом > 125 м 108 108
ВСЕХ домов с разбросом > 125 м 733 889
домов с объявлениями разных городов 53 81 (46 смежных ЕКБ/В.Пышма + 35 далёких)
объявлений, привязанных при пустом адресе 94 / 47 домов 95 / 47 домов
бескоординатных алиасов 7100 (42.6%) 7303 из 21603, из них 6047 без токена города

Три случая разведены

  1. Привязка при пустом адресе — предпосылка опровергнута. Матчинг шёл не «без признака»: fingerprint и new без адреса с номером дома структурно невозможны (алиасов с пустым ключом на проде 0, P1-гейт no_house_number не даёт создать дом). Адрес в момент матчинга был. Дефект в другом: listings.address не обновлялся при ON CONFLICT вообще — строка, вставленная без адреса, оставалась безадресной навсегда. Прод: 862 таких строки. Это же кормит геокодер мусором (худший дом всей таблицы, радиус 2086 км, — одна безадресная карточка, улетевшая в Балтику).
  2. Совпадение улицы в разных городах — дефект ключа, 35 домов. Чинится этим PR.
  3. Большой радиус при верной привязке — дефект геокодера/гранулярности адреса, не сопоставителя. Из 108 коорд-less разбросанных домов межгородских только 8 (все в бэнде > 5 км); остальные 99 внутри одного города — застроечные адреса без настоящего номера («стр. 6.3», «корп. 5», «ГП-1»), где один нормализованный адрес накрывает разные корпуса ЖК. Не трогается.

Потолок, названный числом (в коде)

  • 30 из 35 межгородских склеек закрываются (приходящая карточка областная);
  • 5 из 35 — нет: карточка ЕКБ, а алиас завёл областной дом. Города владельца алиаса мы не знаем — в house_address_aliases его нет. Апгрейд — city-ключ у алиаса: миграция + перекладка 7303 бескоординатных ключей, и до неё нужен журнал слияний;
  • посёлки внутри ЕКБ-развёртки (Кедровка 12.5 км, Б. Седельниково 16.7 км) не ловятся вовсе: у них тот же город-цель «Екатеринбург». Гранулярность независимого наблюдения — город, не населённый пункт. Это ограничение данных.

Размен назван вслух: страж увеличит число New-домов на областных бескоординатных карточках (их 479 из ~50k avito) — то есть подкормит дробление #1772/#2690. Лишний дубль восстановим схлопыванием, межгородская склейка необратима и молча портит всё, что агрегируется по house_id.

Чего НЕ сделано намеренно

  • гео-ограждение 250 м не ослаблено — в #2690 доказано, что оно отвергает пары по существу;
  • уже сшитые дома не расшиваются — разрушительное действие, только после отдельного решения с журналом и обратимостью (house_merge_log);
  • ключ не менялся — иначе перекладка всех бескоординатных алиасов миграцией;
  • число «уберём N дублей» не обещано — обещание числа делает приёмку неисполнимой.

Test plan

  • RED на origin/main: 5 упавших (TypeError: unexpected keyword argument 'city' + недостижимость параметра на границе вызова)
  • GREEN на ветке; полный бэкенд-прогон 4072 passed / 18 skipped
  • контроль-тесты: ЕКБ-развёртка и карточки С координатами идут прежним путём (иначе правка ломала бы 31 663 ЕКБ-карточки ради 35)
  • тест достижимости по образцу #2674 — структурной проверки сигнатуры мало, проверяется реальный вызов city=lot_city
  • ruff + pre-commit + scripts/check-migration-lock-timeout.py (миграций нет): ✓ проверено новых миграций: 3

Критерий приёмки — записан ДО факта

База 2026-08-10: 35 домов с объявлениями далёких пар городов, 35 «чужегородних» объявлений. Из них 20 приросли 08-01, 4 — уже 08-09, то есть счёт растёт.

Проверка 2026-08-17 тем же запросом: если далёких домов ≤ 35 + (новые склейки только из 5 незакрытых направлений и посёлков) — страж работает. Рост по направлению «областная карточка → чужой дом» = не работает. Замер воспроизводится без координат, поэтому не завязан на геокодер.

Второй наблюдаемый признак — строка лога house tier2a/2b skip: coord-less non-ЕКБ city … (sweep_city=…) с непустым sweep_city в tradein-scraper: раньше её на бескоординатных областных карточках не было ни одной.

Refs #2777

## Что чинится Бескоординатная карточка областного формата — «ул. Кирова,4», города в адресе НЕТ — садилась в одноимённый дом другого города. Ключ Tier-2a `address_fingerprint(address, lat, lon)` без координат вырождается в один нормализованный адрес и становится глобально уникальным в `house_address_aliases`. Страж от ровно этой коллизии в матчере **был** (`_skip_oblast_alias`), но выводил город из **текста адреса** — а областной Avito SERP города не называет. Комментарий в коде это и описывал: «a BARE oblast card … carries no signal here … that residual needs sweep-context». **Sweep-context был собран и не доезжал.** `save_listings` знает город-цель batch'а, прогоняет его через собственный гео-guard (`city_anchor`/`city_radius_km`) и пишет в `listings.city`. А `_link_listing_to_house` города не получал, и у `match_or_create_house` такого параметра не было. Признак лежал в соседней колонке той же строки. Город здесь — **независимое наблюдение** (какой город запрашивала развёртка), не второе поле из той же строки адреса. Именно на этом различии в #2690 сорвалось усиление ключа через `gar_house_guid`. ## Перемеренные числа (прод, 2026-08-10, после схлопывания #2704) | замер | issue (08-07) | сегодня | |---|---|---| | домов без координат | 1945 | **706** | | коорд-less домов с разбросом > 125 м | 108 | **108** | | ВСЕХ домов с разбросом > 125 м | 733 | **889** | | домов с объявлениями разных городов | 53 | **81** (46 смежных ЕКБ/В.Пышма + **35 далёких**) | | объявлений, привязанных при пустом адресе | 94 / 47 домов | **95 / 47 домов** | | бескоординатных алиасов | 7100 (42.6%) | **7303 из 21603**, из них **6047** без токена города | ## Три случая разведены 1. **Привязка при пустом адресе — предпосылка опровергнута.** Матчинг шёл не «без признака»: `fingerprint` и `new` без адреса с номером дома структурно невозможны (алиасов с пустым ключом на проде **0**, P1-гейт `no_house_number` не даёт создать дом). Адрес в момент матчинга **был**. Дефект в другом: `listings.address` не обновлялся при `ON CONFLICT` вообще — строка, вставленная без адреса, оставалась безадресной навсегда. Прод: **862** таких строки. Это же кормит геокодер мусором (худший дом всей таблицы, радиус 2086 км, — одна безадресная карточка, улетевшая в Балтику). 2. **Совпадение улицы в разных городах — дефект ключа**, 35 домов. Чинится этим PR. 3. **Большой радиус при верной привязке — дефект геокодера/гранулярности адреса**, не сопоставителя. Из 108 коорд-less разбросанных домов **межгородских только 8** (все в бэнде > 5 км); остальные **99 внутри одного города** — застроечные адреса без настоящего номера («стр. 6.3», «корп. 5», «ГП-1»), где один нормализованный адрес накрывает разные корпуса ЖК. Не трогается. ## Потолок, названный числом (в коде) * **30 из 35** межгородских склеек закрываются (приходящая карточка областная); * **5 из 35** — нет: карточка ЕКБ, а алиас завёл областной дом. Города владельца алиаса мы не знаем — в `house_address_aliases` его нет. Апгрейд — city-ключ у алиаса: миграция + перекладка 7303 бескоординатных ключей, и до неё нужен журнал слияний; * **посёлки внутри ЕКБ-развёртки** (Кедровка 12.5 км, Б. Седельниково 16.7 км) не ловятся вовсе: у них тот же город-цель «Екатеринбург». Гранулярность независимого наблюдения — город, не населённый пункт. Это ограничение данных. Размен назван вслух: страж увеличит число New-домов на областных бескоординатных карточках (их 479 из ~50k avito) — то есть подкормит дробление #1772/#2690. Лишний дубль восстановим схлопыванием, межгородская склейка необратима и молча портит всё, что агрегируется по `house_id`. ## Чего НЕ сделано намеренно * **гео-ограждение 250 м не ослаблено** — в #2690 доказано, что оно отвергает пары по существу; * **уже сшитые дома не расшиваются** — разрушительное действие, только после отдельного решения с журналом и обратимостью (`house_merge_log`); * **ключ не менялся** — иначе перекладка всех бескоординатных алиасов миграцией; * **число «уберём N дублей» не обещано** — обещание числа делает приёмку неисполнимой. ## Test plan - [x] RED на `origin/main`: 5 упавших (`TypeError: unexpected keyword argument 'city'` + недостижимость параметра на границе вызова) - [x] GREEN на ветке; полный бэкенд-прогон 4072 passed / 18 skipped - [x] контроль-тесты: ЕКБ-развёртка и карточки С координатами идут прежним путём (иначе правка ломала бы 31 663 ЕКБ-карточки ради 35) - [x] тест достижимости по образцу #2674 — структурной проверки сигнатуры мало, проверяется реальный вызов `city=lot_city` - [x] ruff + pre-commit + `scripts/check-migration-lock-timeout.py` (миграций нет): `✓ проверено новых миграций: 3` ## Критерий приёмки — записан ДО факта **База 2026-08-10: 35 домов с объявлениями далёких пар городов, 35 «чужегородних» объявлений.** Из них 20 приросли 08-01, 4 — уже 08-09, то есть счёт растёт. **Проверка 2026-08-17** тем же запросом: если далёких домов ≤ 35 + (новые склейки только из 5 незакрытых направлений и посёлков) — страж работает. Рост по направлению «областная карточка → чужой дом» = не работает. Замер воспроизводится без координат, поэтому не завязан на геокодер. Второй наблюдаемый признак — строка лога `house tier2a/2b skip: coord-less non-ЕКБ city … (sweep_city=…)` с непустым `sweep_city` в `tradein-scraper`: раньше её на бескоординатных областных карточках не было ни одной. Refs #2777
bot-backend added 1 commit 2026-08-10 08:25:49 +00:00
fix(tradein/matching): город развёртки доезжает до стража Tier-2a, адрес дозаполняется
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 7s
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 / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m1s
f2b56c52b1
Бескоординатная карточка областного формата («ул. Кирова,4» — города в адресе НЕТ)
садилась в одноимённый дом другого города: ключ Tier-2a = sha256(адрес|lat|lon), без
координат он вырождается в один нормализованный адрес и становится глобально уникальным
в house_address_aliases. Прод 2026-08-10: 7303 из 21603 алиасов — такие ключи, 6047 из них
без токена города вовсе. Страж от этой коллизии в матчере был, но выводил город из ТЕКСТА
адреса (resolve_city_token), а областной SERP города не называет — сам код это и описывал
как «residual, needs sweep-context».

Sweep-context был собран и не доезжал: save_listings знает город-цель batch'а, прогоняет
его через свой гео-guard и пишет в listings.city, а _link_listing_to_house города не
получал и у match_or_create_house такого параметра не было. Признак лежал в соседней
колонке той же строки. Теперь он идёт через границу вызова (Protocol → адаптер → хук),
и его достижимость сторожит тест по образцу #2674 — структурной проверки сигнатуры мало.

Город здесь — НЕЗАВИСИМОЕ наблюдение (какой город запрашивала развёртка), а не второе
поле, выведенное из той же строки адреса: ровно то, на чём в #2690 сорвалось усиление
ключа через gar_house_guid.

Отдельно: listings.address не обновлялся при ON CONFLICT вообще — строка, вставленная без
адреса, оставалась безадресной навсегда, даже когда следующий скрейп адрес приносил (прод:
862 строки). Это и есть «привязка при пустом адресе» из #2777: 95 таких строк имеют
house_id_fk, потому что в момент матчинга адрес БЫЛ (fingerprint/new без адреса структурно
невозможны — алиаса с пустым ключом на проде 0). Дозаполняем COALESCE(existing, new) —
перезапись откатила бы чистку адресов миграциями 062/108/124.

Потолок правки назван числом в коде: 30 из 35 межгородских склеек закрываются, 5 — нет
(приходящая карточка ЕКБ, алиас завёл областной дом; города владельца алиаса мы не знаем).
Посёлки внутри ЕКБ-развёртки (Кедровка, 12 км) не ловятся вовсе — гранулярность признака
город, не населённый пункт. Уже сшитые дома НЕ трогаются.

Refs #2777
bot-backend merged commit 272abac4d2 into main 2026-08-10 08:51:15 +00:00
bot-backend deleted branch fix/2777-cross-city-matching 2026-08-10 08:51:18 +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#2808
No description provided.