[HIGH] tradein/houses: 108 домов сшиты сопоставителем из объявлений разных населённых пунктов (до 290 км разброса) #2777

Open
opened 2026-08-07 08:54:48 +00:00 by bot-backend · 7 comments
Collaborator

Выделено из #2771 отдельной работой: перенос координат эти дома пропускает и считает отдельным счётчиком (skipped_scattered), потому что переносить нечего — неизвестно, какое из объявлений относится к настоящему зданию. Чинить надо сопоставитель, а не перенос.

Числа (прод, 2026-08-07, ST_DistanceSphere — не приближение «градусы × 111320»)

Мера разброса — радиус: максимальное расстояние от центроида объявлений дома до самого дальнего из них. Считалось по домам без координат (lat IS NULL), у которых есть геокодированные объявления — 1490 домов.

разброс (радиус) домов
одна различная точка 1232
<= 125 м +150
125-250 м 21
250-500 м 28
500 м - 5 км 41
> 5 км 18

108 домов имеют радиус > 125 м (порог переноса), из них 18 разъехались на километры. Всего на них 2663 объявления.

Худшие (радиус):

id адрес объявлений радиус
380628 ул. Кирова,4 5 289.9 км
378018 ул. Попова,13 2 156.6 км
380445 Зелёная ул.,5 2 149.4 км
380305 ул. Строителей,1 2 68.6 км
380284 ул. Гагарина,42 2 68.2 км

Адреса говорят сами за себя: «ул. Кирова,4», «ул. Попова,13», «Зелёная ул.,5» — улица+номер без города. Это тот же класс коллизии, что в #2690 (Кедровка/Екатеринбург, Советская 17, 16 км), только виден изнутри одной записи дома, а не между двумя.

Почему это важно за пределами координат

К такому дому привязаны объявления из разных городов. Значит по нему неверна не только точка на карте: цена за м², история, IMV-оценка и всё, что агрегируется по house_id, смешивают разные рынки.

Как воспроизвести замер

WITH pts AS (
  SELECT h.id AS house_id, l.geom
  FROM houses h JOIN listings l ON l.house_id_fk = h.id
  WHERE l.geom IS NOT NULL
), agg AS (
  SELECT house_id, count(*) AS n, ST_Centroid(ST_Collect(geom)) AS cen
  FROM pts GROUP BY house_id
)
SELECT p.house_id, a.n, max(ST_DistanceSphere(p.geom, a.cen)) AS radius_m
FROM pts p JOIN agg a ON a.house_id = p.house_id
GROUP BY p.house_id, a.n
HAVING max(ST_DistanceSphere(p.geom, a.cen)) > 125
ORDER BY 3 DESC;

#2771 замер сделан по домам без координат; тот же запрос без фильтра h.lat IS NULL покажет и дома с координатами — их стоит померить отдельно, оценки там нет.)

Что НЕ делать

Не «поправлять» координаты таким домам усреднением — среднее двух городов не здание. Правильный исход: расщепить запись дома либо не сшивать объявления без совпадения города на этапе сопоставления.

Часть эпика #2674.

Выделено из #2771 отдельной работой: перенос координат эти дома **пропускает** и считает отдельным счётчиком (`skipped_scattered`), потому что переносить нечего — неизвестно, какое из объявлений относится к настоящему зданию. Чинить надо сопоставитель, а не перенос. ## Числа (прод, 2026-08-07, ST_DistanceSphere — не приближение «градусы × 111320») Мера разброса — **радиус**: максимальное расстояние от центроида объявлений дома до самого дальнего из них. Считалось по домам без координат (`lat IS NULL`), у которых есть геокодированные объявления — 1490 домов. | разброс (радиус) | домов | |---|---| | одна различная точка | 1232 | | <= 125 м | +150 | | 125-250 м | 21 | | 250-500 м | 28 | | 500 м - 5 км | 41 | | **> 5 км** | **18** | **108 домов** имеют радиус > 125 м (порог переноса), из них **18 разъехались на километры**. Всего на них 2663 объявления. Худшие (радиус): | id | адрес | объявлений | радиус | |---|---|---|---| | 380628 | ул. Кирова,4 | 5 | 289.9 км | | 378018 | ул. Попова,13 | 2 | 156.6 км | | 380445 | Зелёная ул.,5 | 2 | 149.4 км | | 380305 | ул. Строителей,1 | 2 | 68.6 км | | 380284 | ул. Гагарина,42 | 2 | 68.2 км | Адреса говорят сами за себя: «ул. Кирова,4», «ул. Попова,13», «Зелёная ул.,5» — улица+номер без города. Это тот же класс коллизии, что в #2690 (Кедровка/Екатеринбург, Советская 17, 16 км), только виден **изнутри одной записи дома**, а не между двумя. ## Почему это важно за пределами координат К такому дому привязаны объявления из разных городов. Значит по нему неверна не только точка на карте: цена за м², история, IMV-оценка и всё, что агрегируется по `house_id`, смешивают разные рынки. ## Как воспроизвести замер ```sql WITH pts AS ( SELECT h.id AS house_id, l.geom FROM houses h JOIN listings l ON l.house_id_fk = h.id WHERE l.geom IS NOT NULL ), agg AS ( SELECT house_id, count(*) AS n, ST_Centroid(ST_Collect(geom)) AS cen FROM pts GROUP BY house_id ) SELECT p.house_id, a.n, max(ST_DistanceSphere(p.geom, a.cen)) AS radius_m FROM pts p JOIN agg a ON a.house_id = p.house_id GROUP BY p.house_id, a.n HAVING max(ST_DistanceSphere(p.geom, a.cen)) > 125 ORDER BY 3 DESC; ``` (в #2771 замер сделан по домам без координат; тот же запрос без фильтра `h.lat IS NULL` покажет и дома с координатами — их стоит померить отдельно, оценки там нет.) ## Что НЕ делать Не «поправлять» координаты таким домам усреднением — среднее двух городов не здание. Правильный исход: расщепить запись дома либо не сшивать объявления без совпадения города на этапе сопоставления. Часть эпика #2674.
Author
Collaborator

Разбор: каким тиром сшито, и контрольный замер по домам С координатами

Замерено на проде 2026-08-07, tradein-postgres. Отвечает на два открытых вопроса шапки: «померить дома с координатами отдельно» и «каким тиром сматчены».

1. Контроль: тот же радиус-замер, но по домам С координатами

Тот же запрос, что в шапке, без фильтра h.lat IS NULL:

радиус без координат (шапка) с координатами
одна точка 1231 3035
≤ 125 м 150 3537
125–250 м 21 211
250–500 м 28 143
500 м – 5 км 41 289
> 5 км 18 90
итого > 125 м 108 (2663 объявления) 733 (22 205 объявлений)

Ограничение замера в шапке было существенным: домов с разбросом выше порога 733, а не 108 — в 6.8 раза больше. Но:

Радиус у домов с координатами загрязнён и сам по себе диагнозом не является. Худший дом всей таблицы — 12615 «Ул. Студенческая, секц. 1», радиус 2086.6 км при 113 объявлениях. Из них 112 стоят в одной точке (56.858, 60.658), а 113-е — объявление 293259 с пустым адресом и координатами 59.393 / 24.674 (Балтика). Это дефект геокодера на одной карточке, а не склейка городов. Пустой/NULL-адрес при привязанном доме — 94 объявления на 47 домах.

Поэтому число 733 годится как верхняя граница, а не как счётчик дефектов сопоставителя.

2. Критерий без координат: метка города объявления

Независимо от геометрии: listings.city проставляется скрейпером по городу-цели прогона (save_listings(city=…)), со своим гео-стражем (city_anchor / city_radius_km). Дома, у которых объявления помечены разными городами — 53. Они распадаются на две несмешивающиеся популяции:

популяция домов чужегородних объявлений расстояние до родоначальника метод склейки
ЕКБ ⟷ Верхняя Пышма (смежные) 24 4 2–8 м geo_proximity
далёкие пары городов 29 26 39.8 – 362.8 км fingerprint — 26 из 26

Это и есть проверка критерия: будь метка города шумом, обе популяции выглядели бы одинаково. Смежная пара — то же здание на границе городов, сопоставление верное, расходится только ярлык. Далёкие пары — 26 из 26 склеены вторым тиром по отпечатку адреса, ни одного geo_proximity, ни одного другого метода.

Далёкие пары: ЕКБ|Первоуральск 8, ЕКБ|Н.Тагил 7, ЕКБ|Каменск-Уральский 5, ЕКБ|Серов 4, Каменск-Уральский|Первоуральск 2, В.Пышма|Каменск-Уральский 1, Первоуральск|Серов 1, Каменск-Уральский|Серов 1.

Худшие (расстояние от объявления-родоначальника дома до приклеенного):

дом объявление город карточки город родоначальника метод дата склейки км
380628 10389936 Каменск-Уральский Серов fingerprint 2026-08-06 362.8
378018 10365931 Серов Екатеринбург fingerprint 2026-08-01 313.7
9634 10365983 Серов Екатеринбург fingerprint 2026-08-01 302.2
380445 10365870 Серов Первоуральск fingerprint 2026-08-01 299.2
380305 10365637 Первоуральск Каменск-Уральский fingerprint 2026-08-01 137.3

Все 26 склеек произошли между 2026-07-31 и 2026-08-06 — за семь суток, синхронно с развёрткой области (#2576). До 31 июля межгородских склеек по этому критерию нет ни одной.

Честная граница замера: listings.city пуст у 28 268 из 63 838 привязанных объявлений (44%), сравнить город можно только когда он есть с обеих сторон. 26 — это пол, а не итог.

3. Диагноз: оборванная проводка

Ключ второго тира — address_fingerprint(address, lat, lon) = sha256(нормализованный_адрес | lat:.4f | lon:.4f). У карточки без координат обе координатные части пусты, и ключ вырождается в один нормализованный адрес.

Проверка на проде: у всех трёх худших домов алиас — ровно бескоординатный ключ.

дом normalized_address ключ построен без координат
380628 улица кирова 4 да
378018 улица попова 13 да
380445 зелёная улица 5 да

По всей таблице алиасов: 7100 из 16 660 (42.6%) построены без координат, из них 5841 не содержат токена города — то есть это глобально уникальные ключи вида «улица + номер», в которые попадёт любая бескоординатная карточка с тем же адресом из любого города.

Страж от ровно этой коллизии в матчере есть_skip_oblast_alias в tradein-mvp/backend/app/services/matching/houses.py:221. Но он выводит город из текста адреса через resolve_city_token(norm_addr), а областной формат Avito SERP — «ул. Кирова,4», без города. Токена нет → _resolved_city = None → страж не срабатывает → Tier 2a матчит. Это дословно тот остаток, который сам код и описывает в комментарии houses.py:216-220: «a BARE oblast card … carries no signal here and is deliberately left on the unchanged path; that residual needs sweep-context».

Sweep-context уже собран, просто не доезжает. save_listings знает город-цель батча, прогоняет его через собственный гео-страж и пишет в listings.city (packages/scraper-kit/src/scraper_kit/base.py:399-406). А хук _link_listing_to_house(db, listing_id, lot, matcher) (base.py:838) города не получает вовсе, и у match_or_create_house параметра города нет. Признак собран, лежит в соседней колонке той же строки, и до стража, спроектированного его потреблять, не доходит.

Питание механизма — доля бескоординатных карточек по городам (avito):

город объявлений без координат %
Екатеринбург 29 882 1777 5.9
(город NULL) 16 930 14 270 84.3
Верхняя Пышма 203 117 57.6
Каменск-Уральский 465 151 32.5
Серов 204 65 31.9
Первоуральск 250 46 18.4
Нижний Тагил 658 100 15.2

В области бескоординатных карточек в 3–10 раз больше, чем в ЕКБ. Каждый новый город развёртки кормит дефект сильнее предыдущего.

4. Про «0 из 49 502» у верхних тиров

Свежий счёт: 50 228 строк house_sources, fingerprint 58.93% · new 22.84% · geo_proximity 18.21% · backfill_dedup_merge 9 · backfill_merge 2. cadastr_exact 0, fias_exact 0 — подтверждается.

Но у нуля source_exact причина другая, и её стоит зафиксировать, чтобы не считать третий тир мёртвым: Tier 1 возвращает (house_id, 1.0, "source_exact") из houses.py:189-194 не вызывая _upsert_house_source. Метод физически не может попасть в таблицу. Его 0 — «неприменимо», а не «ни разу не сработал».

5. Что чинить (порядок по цене правки)

  1. Довести город-цель прогона до стража. lot_city уже вычислен в save_listings (со своим гео-стражем), _link_listing_to_house получает lot в том же цикле. Минимальный срез — только страж, без смены ключа: не принимать Tier-2a/2b совпадение, когда город входящей карточки известен и не совпадает с городом владельца алиаса. Правка аддитивная, перекладывать 5841 существующий алиас не требуется.
  2. Не включать город в сам отпечаток первым же шагом. Это меняет ключ → все бескоординатные алиасы надо перекладывать миграцией, и до появления журнала слияний (#2690, п.1) любой массовый проход по домам необратим вслепую.
  3. 29 уже сшитых домов расщепить — но это тоже после журнала слияний: сейчас нельзя ни назвать, что во что свернули, ни откатить.

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

Критерий приёмки, записанный до правки: счёт по критерию из п.2 (дома с объявлениями далёких пар городов) не растёт. База на 2026-08-07 — 29 домов / 26 объявлений, всё приросло за 7 суток. Если через неделю после выкатки счёт прежний — страж работает; если растёт — нет. Замер воспроизводится без координат, поэтому не завязан на геокодер.

Запросы замера: listings JOIN house_sources ON (ext_source, ext_id) — стыкуется у 47 250 из 63 838 привязанных объявлений (74%), из них 46 920 указывают на тот же дом.

## Разбор: каким тиром сшито, и контрольный замер по домам С координатами Замерено на проде 2026-08-07, `tradein-postgres`. Отвечает на два открытых вопроса шапки: «померить дома с координатами отдельно» и «каким тиром сматчены». ### 1. Контроль: тот же радиус-замер, но по домам С координатами Тот же запрос, что в шапке, без фильтра `h.lat IS NULL`: | радиус | без координат (шапка) | **с координатами** | |---|---|---| | одна точка | 1231 | 3035 | | ≤ 125 м | 150 | 3537 | | 125–250 м | 21 | 211 | | 250–500 м | 28 | 143 | | 500 м – 5 км | 41 | 289 | | **> 5 км** | **18** | **90** | | **итого > 125 м** | **108** (2663 объявления) | **733** (22 205 объявлений) | Ограничение замера в шапке было существенным: домов с разбросом выше порога **733, а не 108** — в 6.8 раза больше. Но: **Радиус у домов с координатами загрязнён и сам по себе диагнозом не является.** Худший дом всей таблицы — 12615 «Ул. Студенческая, секц. 1», радиус 2086.6 км при 113 объявлениях. Из них 112 стоят в одной точке (56.858, 60.658), а 113-е — объявление 293259 с **пустым адресом** и координатами 59.393 / 24.674 (Балтика). Это дефект геокодера на одной карточке, а не склейка городов. Пустой/NULL-адрес при привязанном доме — 94 объявления на 47 домах. Поэтому число 733 годится как верхняя граница, а не как счётчик дефектов сопоставителя. ### 2. Критерий без координат: метка города объявления Независимо от геометрии: `listings.city` проставляется скрейпером по городу-цели прогона (`save_listings(city=…)`), со своим гео-стражем (`city_anchor` / `city_radius_km`). Дома, у которых объявления помечены **разными** городами — **53**. Они распадаются на две несмешивающиеся популяции: | популяция | домов | чужегородних объявлений | расстояние до родоначальника | метод склейки | |---|---|---|---|---| | ЕКБ ⟷ Верхняя Пышма (смежные) | 24 | 4 | **2–8 м** | `geo_proximity` | | далёкие пары городов | **29** | **26** | **39.8 – 362.8 км** | **`fingerprint` — 26 из 26** | Это и есть проверка критерия: будь метка города шумом, обе популяции выглядели бы одинаково. Смежная пара — то же здание на границе городов, сопоставление верное, расходится только ярлык. Далёкие пары — 26 из 26 склеены **вторым тиром по отпечатку адреса**, ни одного `geo_proximity`, ни одного другого метода. Далёкие пары: ЕКБ|Первоуральск 8, ЕКБ|Н.Тагил 7, ЕКБ|Каменск-Уральский 5, ЕКБ|Серов 4, Каменск-Уральский|Первоуральск 2, В.Пышма|Каменск-Уральский 1, Первоуральск|Серов 1, Каменск-Уральский|Серов 1. Худшие (расстояние от объявления-родоначальника дома до приклеенного): | дом | объявление | город карточки | город родоначальника | метод | дата склейки | км | |---|---|---|---|---|---|---| | 380628 | 10389936 | Каменск-Уральский | Серов | fingerprint | 2026-08-06 | 362.8 | | 378018 | 10365931 | Серов | Екатеринбург | fingerprint | 2026-08-01 | 313.7 | | 9634 | 10365983 | Серов | Екатеринбург | fingerprint | 2026-08-01 | 302.2 | | 380445 | 10365870 | Серов | Первоуральск | fingerprint | 2026-08-01 | 299.2 | | 380305 | 10365637 | Первоуральск | Каменск-Уральский | fingerprint | 2026-08-01 | 137.3 | **Все 26 склеек произошли между 2026-07-31 и 2026-08-06** — за семь суток, синхронно с развёрткой области (#2576). До 31 июля межгородских склеек по этому критерию нет ни одной. Честная граница замера: `listings.city` пуст у **28 268 из 63 838** привязанных объявлений (44%), сравнить город можно только когда он есть с обеих сторон. **26 — это пол, а не итог.** ### 3. Диагноз: оборванная проводка Ключ второго тира — `address_fingerprint(address, lat, lon)` = `sha256(нормализованный_адрес | lat:.4f | lon:.4f)`. У карточки **без координат** обе координатные части пусты, и ключ вырождается в один нормализованный адрес. Проверка на проде: у всех трёх худших домов алиас — ровно бескоординатный ключ. | дом | normalized_address | ключ построен без координат | |---|---|---| | 380628 | `улица кирова 4` | да | | 378018 | `улица попова 13` | да | | 380445 | `зелёная улица 5` | да | По всей таблице алиасов: **7100 из 16 660 (42.6%) построены без координат**, из них **5841 не содержат токена города** — то есть это глобально уникальные ключи вида «улица + номер», в которые попадёт любая бескоординатная карточка с тем же адресом из любого города. Страж от ровно этой коллизии в матчере **есть** — `_skip_oblast_alias` в `tradein-mvp/backend/app/services/matching/houses.py:221`. Но он выводит город из **текста адреса** через `resolve_city_token(norm_addr)`, а областной формат Avito SERP — «ул. Кирова,4», без города. Токена нет → `_resolved_city = None` → страж не срабатывает → Tier 2a матчит. Это дословно тот остаток, который сам код и описывает в комментарии `houses.py:216-220`: «a BARE oblast card … carries no signal here and is deliberately left on the unchanged path; that residual needs sweep-context». **Sweep-context уже собран, просто не доезжает.** `save_listings` знает город-цель батча, прогоняет его через собственный гео-страж и пишет в `listings.city` (`packages/scraper-kit/src/scraper_kit/base.py:399-406`). А хук `_link_listing_to_house(db, listing_id, lot, matcher)` (`base.py:838`) города не получает вовсе, и у `match_or_create_house` параметра города нет. Признак собран, лежит в соседней колонке той же строки, и до стража, спроектированного его потреблять, не доходит. Питание механизма — доля бескоординатных карточек по городам (avito): | город | объявлений | без координат | % | |---|---|---|---| | Екатеринбург | 29 882 | 1777 | 5.9 | | (город NULL) | 16 930 | 14 270 | 84.3 | | Верхняя Пышма | 203 | 117 | 57.6 | | Каменск-Уральский | 465 | 151 | 32.5 | | Серов | 204 | 65 | 31.9 | | Первоуральск | 250 | 46 | 18.4 | | Нижний Тагил | 658 | 100 | 15.2 | В области бескоординатных карточек в 3–10 раз больше, чем в ЕКБ. Каждый новый город развёртки кормит дефект сильнее предыдущего. ### 4. Про «0 из 49 502» у верхних тиров Свежий счёт: 50 228 строк `house_sources`, `fingerprint` 58.93% · `new` 22.84% · `geo_proximity` 18.21% · `backfill_dedup_merge` 9 · `backfill_merge` 2. `cadastr_exact` 0, `fias_exact` 0 — подтверждается. Но у нуля `source_exact` причина **другая, и её стоит зафиксировать, чтобы не считать третий тир мёртвым**: Tier 1 возвращает `(house_id, 1.0, "source_exact")` из `houses.py:189-194` **не вызывая `_upsert_house_source`**. Метод физически не может попасть в таблицу. Его 0 — «неприменимо», а не «ни разу не сработал». ### 5. Что чинить (порядок по цене правки) 1. **Довести город-цель прогона до стража.** `lot_city` уже вычислен в `save_listings` (со своим гео-стражем), `_link_listing_to_house` получает `lot` в том же цикле. Минимальный срез — **только страж, без смены ключа**: не принимать Tier-2a/2b совпадение, когда город входящей карточки известен и не совпадает с городом владельца алиаса. Правка аддитивная, перекладывать 5841 существующий алиас не требуется. 2. **Не включать город в сам отпечаток** первым же шагом. Это меняет ключ → все бескоординатные алиасы надо перекладывать миграцией, и до появления журнала слияний (#2690, п.1) любой массовый проход по домам необратим вслепую. 3. **29 уже сшитых домов расщепить** — но это тоже после журнала слияний: сейчас нельзя ни назвать, что во что свернули, ни откатить. **Риск правки, который надо назвать вслух:** страж по городу увеличит число New-домов, то есть подкормит дробление из #2690/#1772. Размен осознанный — лишний дубль восстановим схлопыванием, межгородская склейка необратима и портит агрегаты по `house_id` (цена м², история, IMV) молча. **Критерий приёмки, записанный до правки:** счёт по критерию из п.2 (дома с объявлениями далёких пар городов) не растёт. База на 2026-08-07 — **29 домов / 26 объявлений**, всё приросло за 7 суток. Если через неделю после выкатки счёт прежний — страж работает; если растёт — нет. Замер воспроизводится без координат, поэтому не завязан на геокодер. <sub>Запросы замера: `listings JOIN house_sources ON (ext_source, ext_id)` — стыкуется у 47 250 из 63 838 привязанных объявлений (74%), из них 46 920 указывают на тот же дом.</sub>
Author
Collaborator

Перемерено на сегодняшних данных (прод, 2026-08-10 08:06 UTC), правка на проде

Схлопывание #2704 и перенос координат #2771 картину сдвинули — часть чисел шапки устарела, часть нет.

замер шапка / комментарий (08-07) сегодня (08-10)
домов всего 9625 9179
домов без координат 1945 706
коорд-less домов, разброс > 125 м 108 108 (2892 объявления)
ВСЕХ домов, разброс > 125 м 733 889 (верхняя граница, не счётчик дефектов)
домов с объявлениями разных городов 53 81: 46 смежных ЕКБ⟷В.Пышма + 35 далёких
«чужегородних» объявлений на далёких домах 26 35
привязка при пустом адресе 94 / 47 домов 95 / 47 домов
бескоординатных алиасов 7100 из 16660 7303 из 21603, из них 6047 вообще без токена города

Число 108 совпало не случайно: перенос координат забрал из популяции «без координат» ровно согласные дома, разбросанные остались.

Три случая разведены — их не поровну

1. Привязка при пустом адресе — предпосылка опровергнута. «Привязка состоялась без признака, по которому должна была» — не подтверждается. Проверка: fingerprint требует алиас с непустым нормализованным адресом, а алиасов с пустым ключом (left(sha256('||'),32)) на проде 0; new заблокирован P1-гейтом no_house_number. Обе метки структурно невозможны без адреса С НОМЕРОМ ДОМА — а у 95 таких строк стоит именно fingerprint (35), new (8), geo_proximity (1), остальные — исторический backfill (52 yandex-строки от 2026-05-27).

Значит адрес в момент матчинга был. Настоящий дефект — в другом месте: listings.address не обновлялся при ON CONFLICT вообще. Строка, вставленная без адреса, оставалась безадресной навсегда, даже когда следующий скрейп адрес приносил. Прод: 862 строки с address IS NULL, blank-строк 0. Это же кормит геокодер: худший дом всей таблицы (радиус 2086 км) — одна безадресная карточка, улетевшая в Балтику.

2. Совпадение улицы в разных городах — дефект ключа. 35 домов. Направление важно: у 24 родоначальник ЕКБ (приходит областная карточка), у 11 родоначальник областной. Метод склейки — fingerprint у 32 из 35 (у 3 нет строки house_sources). Осторожно с одним артефактом: 21 из 32 «приклеенных» объявлений СЕЙЧАС имеют координаты, но это не значит, что они были у них в момент матчинга — geoportal_coords_backfill дописывает координаты позже. Ключ Tier-2a без координат и есть механизм.

3. Большой радиус при верной привязке — дефект геокодера, не сопоставителя. Из 108 коорд-less разбросанных домов межгородских (по метке listings.city) — всего 8, и все они в бэнде > 5 км. Остальные 99 внутри одного города: застроечные адреса без настоящего номера — «Ул. Лучистая-Нескучная, стр. 6.3» (110 объявлений, 9.7 км), «Ул. Василия Шамарина» (117, 5.7 км), «Кв-л 45, стр. 1.4Б» (22, 8.5 км). Один нормализованный адрес накрывает разные корпуса ЖК. Это не межгородская склейка и одной правкой с ней не чинится.

Отдельная подгруппа внутри этих 99 — посёлки в границах ЕКБ-развёртки: Кедровка (Советская 20, 12.5 км — та самая коллизия из #2690), Б. Седельниково (16.7 км), Решёты (13.4 км). Дефект тот же, что в п.2, но признак города их не видит: город-цель у них «Екатеринбург» с обеих сторон.

Где терялся город — доказательство из кода origin/main

Страж существует: matching/houses.py, _skip_oblast_alias. Город он берёт из текста адресаresolve_city_token(norm_addr). Областной формат Avito SERP («ул. Кирова,4») города не называет → None → страж молчит. Сам код это и описывал: «a BARE oblast card … carries no signal here and is deliberately left on the unchanged path; that residual needs sweep-context».

Sweep-context был собран и не доезжал: save_listings вычисляет lot_city (город-цель batch'а, прогнанный через собственный гео-guard city_anchor/city_radius_km) и пишет его в listings.city, а вызывала она _link_listing_to_house(db, listing_id, lot, matcher) — без города; параметра city у match_or_create_house не было вовсе. Признак лежал в соседней колонке той же строки и не доходил до стража, спроектированного его потреблять. Смежное наблюдение из #2657 подтверждено на актуальном коде.

Что сделано — PR #2808 (смержен, на проде)

  1. city идёт через всю границу вызова: save_listings_link_listing_to_house → Protocol HouseMatcherRealMatcherAdaptermatch_or_create_house. Достижимость сторожит тест по образцу #2674 — структурной сверки сигнатуры мало, проверяется реальная передача city=lot_city.
  2. Страж — тот же, ключ не менялся: перекладывать 7303 бескоординатных алиаса миграцией не требуется.
  3. Тот же признак подан второму вызывающему, у которого он есть, — cian/detail.py читает city из той же строки листинга.
  4. address = COALESCE(listings.address, EXCLUDED.address) — дозаполнение, существующее значение выигрывает: перезапись молча откатила бы чистку адресов миграциями 062/108/124.

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

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

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

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

Чего осознанно НЕ сделано

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

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

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

Проверка 2026-08-17 тем же запросом (listings.city + listing_sources.matched_at, смежная пара ЕКБ⟷В.Пышма исключается):

  • далёких домов ≤ 35 плюс только новые случаи из пяти незакрытых направлений и посёлков — страж работает;
  • рост по направлению «областная карточка → дом другого города» — не работает.

Замер воспроизводится без координат, поэтому не завязан на геокодер. Второй наблюдаемый признак — строка house tier2a/2b skip: coord-less non-ЕКБ city … (sweep_city=…) в логах tradein-scraper с непустым sweep_city: до правки таких не было ни одной. На момент публикации ни одной развёртки после деплоя ещё не прошло (ближайшая крупная — avito_full_load 14:16 UTC), поэтому наблюдение отложено, а не «проверено».

Правка на проде проверена по КОДУ в живом контейнере: city в сигнатуре матчера, city=city в хуке, city=lot_city в save_listings, дозаполнение адреса — все четыре присутствуют в tradein-scraper.

Замеры: docker exec tradein-postgres psql -U tradein -d tradein (только чтение), файлы запросов — m2777_a…e.sql.

## Перемерено на сегодняшних данных (прод, 2026-08-10 08:06 UTC), правка на проде Схлопывание #2704 и перенос координат #2771 картину сдвинули — часть чисел шапки устарела, часть нет. | замер | шапка / комментарий (08-07) | сегодня (08-10) | |---|---|---| | домов всего | 9625 | **9179** | | домов без координат | 1945 | **706** | | коорд-less домов, разброс > 125 м | 108 | **108** (2892 объявления) | | ВСЕХ домов, разброс > 125 м | 733 | **889** (верхняя граница, не счётчик дефектов) | | домов с объявлениями разных городов | 53 | **81**: 46 смежных ЕКБ⟷В.Пышма + **35 далёких** | | «чужегородних» объявлений на далёких домах | 26 | **35** | | привязка при пустом адресе | 94 / 47 домов | **95 / 47 домов** | | бескоординатных алиасов | 7100 из 16660 | **7303 из 21603**, из них **6047** вообще без токена города | Число 108 совпало не случайно: перенос координат забрал из популяции «без координат» ровно согласные дома, разбросанные остались. ## Три случая разведены — их не поровну **1. Привязка при пустом адресе — предпосылка опровергнута.** «Привязка состоялась без признака, по которому должна была» — не подтверждается. Проверка: `fingerprint` требует алиас с непустым нормализованным адресом, а алиасов с пустым ключом (`left(sha256('||'),32)`) на проде **0**; `new` заблокирован P1-гейтом `no_house_number`. Обе метки структурно невозможны без адреса С НОМЕРОМ ДОМА — а у 95 таких строк стоит именно `fingerprint` (35), `new` (8), `geo_proximity` (1), остальные — исторический `backfill` (52 yandex-строки от 2026-05-27). Значит адрес в момент матчинга **был**. Настоящий дефект — в другом месте: `listings.address` **не обновлялся при `ON CONFLICT` вообще**. Строка, вставленная без адреса, оставалась безадресной навсегда, даже когда следующий скрейп адрес приносил. Прод: **862** строки с `address IS NULL`, blank-строк 0. Это же кормит геокодер: худший дом всей таблицы (радиус 2086 км) — одна безадресная карточка, улетевшая в Балтику. **2. Совпадение улицы в разных городах — дефект ключа. 35 домов.** Направление важно: у **24** родоначальник ЕКБ (приходит областная карточка), у **11** родоначальник областной. Метод склейки — `fingerprint` у 32 из 35 (у 3 нет строки `house_sources`). Осторожно с одним артефактом: 21 из 32 «приклеенных» объявлений СЕЙЧАС имеют координаты, но это не значит, что они были у них в момент матчинга — `geoportal_coords_backfill` дописывает координаты позже. Ключ Tier-2a без координат и есть механизм. **3. Большой радиус при верной привязке — дефект геокодера, не сопоставителя.** Из 108 коорд-less разбросанных домов межгородских (по метке `listings.city`) — **всего 8**, и все они в бэнде > 5 км. Остальные **99 внутри одного города**: застроечные адреса без настоящего номера — «Ул. Лучистая-Нескучная, стр. 6.3» (110 объявлений, 9.7 км), «Ул. Василия Шамарина» (117, 5.7 км), «Кв-л 45, стр. 1.4Б» (22, 8.5 км). Один нормализованный адрес накрывает разные корпуса ЖК. Это не межгородская склейка и одной правкой с ней не чинится. Отдельная подгруппа внутри этих 99 — **посёлки в границах ЕКБ-развёртки**: Кедровка (Советская 20, 12.5 км — та самая коллизия из #2690), Б. Седельниково (16.7 км), Решёты (13.4 км). Дефект тот же, что в п.2, но признак города их **не видит**: город-цель у них «Екатеринбург» с обеих сторон. ## Где терялся город — доказательство из кода `origin/main` Страж существует: `matching/houses.py`, `_skip_oblast_alias`. Город он берёт из **текста адреса** — `resolve_city_token(norm_addr)`. Областной формат Avito SERP («ул. Кирова,4») города не называет → `None` → страж молчит. Сам код это и описывал: «a BARE oblast card … carries no signal here and is deliberately left on the unchanged path; that residual needs sweep-context». Sweep-context **был собран и не доезжал**: `save_listings` вычисляет `lot_city` (город-цель batch'а, прогнанный через собственный гео-guard `city_anchor`/`city_radius_km`) и пишет его в `listings.city`, а вызывала она `_link_listing_to_house(db, listing_id, lot, matcher)` — без города; параметра `city` у `match_or_create_house` не было вовсе. Признак лежал в соседней колонке той же строки и не доходил до стража, спроектированного его потреблять. Смежное наблюдение из #2657 подтверждено на актуальном коде. ## Что сделано — PR #2808 (смержен, на проде) 1. `city` идёт через всю границу вызова: `save_listings` → `_link_listing_to_house` → Protocol `HouseMatcher` → `RealMatcherAdapter` → `match_or_create_house`. Достижимость сторожит тест по образцу #2674 — структурной сверки сигнатуры мало, проверяется реальная передача `city=lot_city`. 2. Страж — тот же, ключ **не менялся**: перекладывать 7303 бескоординатных алиаса миграцией не требуется. 3. Тот же признак подан второму вызывающему, у которого он есть, — `cian/detail.py` читает `city` из той же строки листинга. 4. `address = COALESCE(listings.address, EXCLUDED.address)` — дозаполнение, существующее значение выигрывает: перезапись молча откатила бы чистку адресов миграциями 062/108/124. Город здесь — **независимое наблюдение** (какой город запрашивала развёртка), а не второе поле, выведенное из той же строки адреса. Ровно на этом различии в #2690 сорвалось усиление ключа через `gar_house_guid`. ## Потолок назван числом (и записан в коде) * **30 из 35** межгородских склеек закрываются — приходящая карточка областная; * **5 из 35** — нет: карточка ЕКБ, а алиас завёл областной дом. Города владельца алиаса мы не знаем, в `house_address_aliases` его нет. Апгрейд — city-ключ у алиаса, но это миграция + перекладка 7303 ключей, и до неё нужен журнал слияний (#2690 п.1); * посёлки внутри ЕКБ-развёртки — **не ловятся вовсе**. Гранулярность независимого наблюдения — город, не населённый пункт. Это ограничение данных, а не недоделка стража. Независимого источника уровня «населённый пункт» у нас нет — придумывать его из того же адреса нельзя. Размен назван вслух: страж добавит New-домов на областных бескоординатных карточках (их 479 из ~50 тыс. avito) — то есть подкормит дробление #1772/#2690. ## Чего осознанно НЕ сделано * **гео-ограждение 250 м не тронуто** — в #2690 доказано, что оно отвергает пары по существу; * **уже сшитые 35 домов не расшиваются.** Расшивка — разрушительное действие; делать её только с журналом и обратимостью по образцу `house_merge_log`, порционно. Видимых пользователю изменений было бы **35 объявлений на 35 домах** (сам дом при этом теряет часть истории цены за м² и IMV). Решение — за владельцем, отдельной задачей; * **число «уберём N дублей» не обещано** — обещание числа делает приёмку неисполнимой. ## Критерий приёмки — записан ДО факта **База 2026-08-10: 35 домов с объявлениями далёких пар городов, 35 чужегородних объявлений.** Динамика: 20 из них приросли 08-01, 3 — 08-06, 4 — уже 08-09. Счёт растёт, значит замер живой. **Проверка 2026-08-17** тем же запросом (`listings.city` + `listing_sources.matched_at`, смежная пара ЕКБ⟷В.Пышма исключается): * далёких домов ≤ 35 плюс только новые случаи из пяти незакрытых направлений и посёлков — **страж работает**; * рост по направлению «областная карточка → дом другого города» — **не работает**. Замер воспроизводится без координат, поэтому не завязан на геокодер. Второй наблюдаемый признак — строка `house tier2a/2b skip: coord-less non-ЕКБ city … (sweep_city=…)` в логах `tradein-scraper` с непустым `sweep_city`: до правки таких не было ни одной. На момент публикации ни одной развёртки после деплоя ещё не прошло (ближайшая крупная — `avito_full_load` 14:16 UTC), поэтому наблюдение отложено, а не «проверено». Правка на проде проверена по КОДУ в живом контейнере: `city` в сигнатуре матчера, `city=city` в хуке, `city=lot_city` в `save_listings`, дозаполнение адреса — все четыре присутствуют в `tradein-scraper`. <sub>Замеры: `docker exec tradein-postgres psql -U tradein -d tradein` (только чтение), файлы запросов — `m2777_a…e.sql`.</sub>
Author
Collaborator

Поправка: моя формулировка про «пустой адрес» была неверна

Я написал в шапке и владельцу: «95 объявлений на 47 домах привязаны при пустом адресе — привязка состоялась без признака, по которому должна была». Это неверно, и вывод из этого следовал бы неправильный.

Адрес в момент сопоставления был. Обе метки, которыми эти строки привязаны, без адреса структурно невозможны: fingerprint требует алиас с непустым адресом (алиасов с пустым ключом на проде 0), а создание нового дома заблокировано гейтом no_house_number. Пуста не привязка — пуста колонка.

Настоящий дефект другой и хуже: listings.address не обновлялся при ON CONFLICT вообще. Строка, вставленная без адреса (областная карточка Авито, где адрес не в выдаче), оставалась безадресной навсегда — даже когда следующий проход приносил адрес.

Замер на проде 2026-08-10:

объявлений без адреса 862 из 97 813 (0.88%)
avito 765, из них активных 360
yandex 97, из них активных 10
виделись свипом за последние двое суток 244

Последняя строка важна: после правки (address = COALESCE(listings.address, EXCLUDED.address)) эти 244 дозаполнятся сами на ближайших проходах, без миграции и без разового скрипта.

Тот же дефект кормил геокодер: дом с радиусом 2086 км — это одна безадресная карточка, уехавшая координатами в Балтийское море.

Три случая оказались очень неравными

Я предполагал, что главное — ключ без города. Разбивка 108 разбросанных домов:

случай сколько что чинить
дефект геокодера 99 из 108 не сопоставитель: один нормализованный адрес на разные корпуса ЖК («стр. 6.3», «корп. 5», «ГП-1»)
межгородские 8 из 108 ключ без города
в том числе посёлки внутри ЕКБ 4 не ловится в принципе — городского признака у них нет

То есть починка ключа закрывает меньше десятой части разброса. Это стоило узнать до правки, а не после.

Где терялся город

Он есть и доезжает до соседней колонки: save_listings считает город-цель развёртки и пишет его в listings.city. Но вызывает _link_listing_to_house без города, а у match_or_create_house параметра города не было вовсе. Страж берёт город из текста адреса, а областная выдача Авито («ул. Кирова, 4») города не называет.

Код сам это признавал в комментарии: «этому остатку нужен контекст развёртки».

Потолок назван числом, а не обойдён

Из 35 домов с чужегородними объявлениями правка закрывает 30. Пять — нет: карточка ЕКБ, алиас завёл областной дом, города владельца алиаса мы не знаем. Посёлки в границах развёртки не ловятся вовсе.

Придумывать городской признак из той же строки адреса нельзя — в #2690 доказано, что ключ, выведенный из того же источника, что и защита, её отменяет.

Ключ не менялся, дома не расшивались

Перекладка 7303 алиасов не потребовалась. Расшивка сшитого — разрушительное действие; если владелец захочет, это 35 объявлений на 35 домах, только с журналом и обратимостью по образцу house_merge_log.

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

База 2026-08-10: 35 далёких домов / 35 чужегородних объявлений. Проверка 2026-08-17: рост по направлению «областная карточка → дом другого города» означает, что страж не работает. Замер не зависит от геокодера.

Честная оговорка: на момент отчёта ни одной областной развёртки после деплоя не прошло, поэтому это отложенное наблюдение, а не «проверено».

## Поправка: моя формулировка про «пустой адрес» была неверна Я написал в шапке и владельцу: «95 объявлений на 47 домах **привязаны при пустом адресе** — привязка состоялась без признака, по которому должна была». Это неверно, и вывод из этого следовал бы неправильный. **Адрес в момент сопоставления был.** Обе метки, которыми эти строки привязаны, без адреса структурно невозможны: `fingerprint` требует алиас с непустым адресом (алиасов с пустым ключом на проде **0**), а создание нового дома заблокировано гейтом `no_house_number`. Пуста не привязка — пуста **колонка**. Настоящий дефект другой и хуже: **`listings.address` не обновлялся при `ON CONFLICT` вообще**. Строка, вставленная без адреса (областная карточка Авито, где адрес не в выдаче), оставалась безадресной **навсегда** — даже когда следующий проход приносил адрес. Замер на проде 2026-08-10: | | | |---|---| | объявлений без адреса | **862** из 97 813 (0.88%) | | avito | 765, из них активных 360 | | yandex | 97, из них активных 10 | | **виделись свипом за последние двое суток** | **244** | Последняя строка важна: после правки (`address = COALESCE(listings.address, EXCLUDED.address)`) эти 244 дозаполнятся сами на ближайших проходах, без миграции и без разового скрипта. Тот же дефект кормил геокодер: дом с радиусом 2086 км — это одна безадресная карточка, уехавшая координатами в Балтийское море. ## Три случая оказались очень неравными Я предполагал, что главное — ключ без города. Разбивка 108 разбросанных домов: | случай | сколько | что чинить | |---|---|---| | **дефект геокодера** | **99 из 108** | не сопоставитель: один нормализованный адрес на разные корпуса ЖК («стр. 6.3», «корп. 5», «ГП-1») | | межгородские | **8 из 108** | ключ без города | | в том числе посёлки внутри ЕКБ | 4 | **не ловится в принципе** — городского признака у них нет | То есть починка ключа закрывает меньше десятой части разброса. Это стоило узнать до правки, а не после. ## Где терялся город Он **есть** и **доезжает до соседней колонки**: `save_listings` считает город-цель развёртки и пишет его в `listings.city`. Но вызывает `_link_listing_to_house` **без города**, а у `match_or_create_house` параметра города не было вовсе. Страж берёт город из **текста адреса**, а областная выдача Авито («ул. Кирова, 4») города не называет. Код сам это признавал в комментарии: «этому остатку нужен контекст развёртки». ## Потолок назван числом, а не обойдён Из 35 домов с чужегородними объявлениями правка закрывает **30**. Пять — нет: карточка ЕКБ, алиас завёл областной дом, города владельца алиаса мы не знаем. Посёлки в границах развёртки не ловятся вовсе. Придумывать городской признак из той же строки адреса **нельзя** — в #2690 доказано, что ключ, выведенный из того же источника, что и защита, её отменяет. ## Ключ не менялся, дома не расшивались Перекладка 7303 алиасов не потребовалась. Расшивка сшитого — разрушительное действие; если владелец захочет, это **35 объявлений на 35 домах**, только с журналом и обратимостью по образцу `house_merge_log`. ## Критерий приёмки записан ДО факта База 2026-08-10: **35 далёких домов / 35 чужегородних объявлений**. Проверка **2026-08-17**: рост по направлению «областная карточка → дом другого города» означает, что страж не работает. Замер не зависит от геокодера. Честная оговорка: на момент отчёта ни одной областной развёртки после деплоя не прошло, поэтому это **отложенное наблюдение, а не «проверено»**.
Author
Collaborator

Промежуточная проверка стража города — досрочно, 12.08 вместо 17.08

Критерий был записан на 17.08, но проверяю раньше, потому что данных уже достаточно, а результат влияет на приоритет.

Правильное поле: matched_at, а не scraped_at

Первый мой замер дал «39 объявлений привязано к дому дальше 10 км после стража» — и это было бы неверным прочтением. scraped_at двигается при переподтверждении объявления (#2206), поэтому привязка, сделанная в июле, у переувиденного сегодня объявления выглядит свежей.

Настоящий момент сопоставления лежит в listing_sources.matched_at. По нему:

привязок
сопоставлено до стража (< 10.08 09:00) 886
сопоставлено после стража 4
всего привязок сделано после стража 1348

Самое свежее ошибочное сопоставление — 11.08 03:12.

Оценка

Четыре ошибки на 1348 привязок — 0.3%. Против 886 накопленных за всё время до стража. Страж работает.

Но не ноль, и это ожидаемо: в разборе честно сказано, что правка закрывает 30 случаев из 35, а посёлки внутри городской развёртки не ловятся в принципе — независимого признака населённого пункта у нас нет. Четыре свежих ошибки укладываются в названный потолок.

Отдельно: моя ложная тревога, снятая тем же приёмом

Сегодня при обзоре я замерил «113 домов с разбросом больше 10 км» против базовых 17 и чуть не объявил рост в семь раз. Пересчитал на той же популяции, что при базовом замере (дома без координат):

было 17 (07.08)  →  сейчас 17
было 108 (10.08) →  сейчас 108

Ровно те же числа. «Рост» был артефактом знаменателя: после переноса координат (#2771) у 1382 домов появился измеримый разброс, которого раньше не было — популяция выросла, а не дефект.

Это второй раз за сегодня, когда я едва не отчитался о регрессе, сравнив разные популяции. Первый был с долей геокодирования.

Что остаётся

886 исторических ошибочных привязок не расшиваются сами. Расшивка — разрушительное действие, и в разборе названо число видимых изменений. Решение владельца; я не предлагаю без запроса.

Задачу оставляю открытой до 17.08 — критерий записан на эту дату, и досрочная проверка его не отменяет.

## Промежуточная проверка стража города — досрочно, 12.08 вместо 17.08 Критерий был записан на 17.08, но проверяю раньше, потому что данных уже достаточно, а результат влияет на приоритет. ### Правильное поле: `matched_at`, а не `scraped_at` Первый мой замер дал «39 объявлений привязано к дому дальше 10 км после стража» — и это было бы **неверным** прочтением. `scraped_at` двигается при **переподтверждении** объявления (#2206), поэтому привязка, сделанная в июле, у переувиденного сегодня объявления выглядит свежей. Настоящий момент сопоставления лежит в `listing_sources.matched_at`. По нему: | | привязок | |---|---:| | сопоставлено **до** стража (< 10.08 09:00) | **886** | | сопоставлено **после** стража | **4** | | всего привязок сделано после стража | 1348 | Самое свежее ошибочное сопоставление — **11.08 03:12**. ### Оценка Четыре ошибки на 1348 привязок — **0.3%**. Против 886 накопленных за всё время до стража. Страж работает. Но **не ноль**, и это ожидаемо: в разборе честно сказано, что правка закрывает 30 случаев из 35, а посёлки внутри городской развёртки не ловятся **в принципе** — независимого признака населённого пункта у нас нет. Четыре свежих ошибки укладываются в названный потолок. ### Отдельно: моя ложная тревога, снятая тем же приёмом Сегодня при обзоре я замерил «113 домов с разбросом больше 10 км» против базовых 17 и чуть не объявил рост в семь раз. Пересчитал **на той же популяции**, что при базовом замере (дома без координат): ``` было 17 (07.08) → сейчас 17 было 108 (10.08) → сейчас 108 ``` Ровно те же числа. «Рост» был артефактом знаменателя: после переноса координат (#2771) у 1382 домов появился измеримый разброс, которого раньше не было — популяция выросла, а не дефект. Это второй раз за сегодня, когда я едва не отчитался о регрессе, сравнив разные популяции. Первый был с долей геокодирования. ### Что остаётся 886 исторических ошибочных привязок не расшиваются сами. Расшивка — разрушительное действие, и в разборе названо число видимых изменений. Решение владельца; я не предлагаю без запроса. Задачу оставляю открытой до 17.08 — критерий записан на эту дату, и досрочная проверка его не отменяет.
Author
Collaborator

Независимое наблюдение всё-таки есть: город объявления против кадастра дома

10.08 в разборе #2690 мы установили, что независимого признака идентичности здания у нас нет — все кандидаты (кадастр, ФИАС, идентификаторы реестров) выведены из той же строки адреса, что и основной ключ. Вывод был честный, и он остаётся верным для задачи схлопывания дублей.

Но сегодня, проверяя побочную находку из #2583, я наткнулся на признак, который в тот разбор не попал, — и он не выводится из адреса.

Дома-химеры: одна запись, объявления из разных городов

домов, к которым привязаны объявления из разных городов 82
домов с объявлениями вообще 8218
доля 1.0%
из них с кадастром (противоречие проверяемо вдвойне) 21

Живые примеры, все с екатеринбургским кадастровым округом 66:41:

9618    р-н Орджоникидзевский, мкр. Уралмаш   66:41:0106108:104   Екатеринбург + Нижний Тагил
372796  р-н Чкаловский, мкр. Химмаш           66:41:0509002:38    Екатеринбург + Каменск-Уральский
9701    р-н Орджоникидзевский, мкр. Эльмаш    66:41:0108001:381   Верхняя Пышма + Екатеринбург
372859  р-н Кировский, мкр. Втузгородок       66:41:0704024:29    Екатеринбург + Нижний Тагил
192337  Екатеринбург, улица Чекистов, 9       66:41:0702058:91    Екатеринбург + Первоуральск

Почему это независимое наблюдение

listings.city заполняется городом-целью развёртки (какую площадку и какой город мы обходили), а не разбором адресной строки. Кадастровый округ дома приходит из внешнего справочника. Это два разных источника, и их противоречие — настоящее свидетельство, а не тавтология.

Именно этого не хватало в #2690: там все кандидаты сводились к одной строке, поэтому усиление ключа отменяло защиту. Здесь — нет.

Чем это отличается от уже измеренного

В #2777 разброс мерился географически — расстояние между координатами объявлений одного дома. Тот замер видит только дома, у которых координаты есть, и не различает «геокодер промахнулся» от «сшиты разные здания».

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

Что предлагается — и чего НЕ предлагается

Не предлагаю использовать это как ключ схлопывания: вывод #2690 остаётся в силе, склеивать дома по этому признаку нечем.

Предлагаю использовать как детектор: 82 дома — готовый список кандидатов на разбор, из них 21 с двойным свидетельством. Это в разы уже, чем 906 домов с разбросом больше 250 метров, и точнее — среди них нет промахов геокодера.

Отдельный вопрос — стоит ли запрещать привязку, когда город объявления противоречит уже привязанным. Это меняет поведение сопоставителя и требует замера: сколько привязок отклонится и сколько из них были верными.

Поправка к формулировке из разбора #2583

Там сказано «18 областных лотов несут кадастры 66:41». Проверил: кадастры у объявлений не заполнены вообще (0 из 99 167), они есть только у домов (2648 из 9461). Дефект существует, но он о другом — не лоты несут чужой кадастр, а дом собирает лоты из разных городов. Формулировка вводила бы в заблуждение при поиске.

## Независимое наблюдение всё-таки есть: город объявления против кадастра дома 10.08 в разборе #2690 мы установили, что **независимого признака идентичности здания у нас нет** — все кандидаты (кадастр, ФИАС, идентификаторы реестров) выведены из той же строки адреса, что и основной ключ. Вывод был честный, и он остаётся верным **для задачи схлопывания дублей**. Но сегодня, проверяя побочную находку из #2583, я наткнулся на признак, который в тот разбор не попал, — и он **не выводится из адреса**. ### Дома-химеры: одна запись, объявления из разных городов | | | |---|---:| | домов, к которым привязаны объявления из **разных городов** | **82** | | домов с объявлениями вообще | 8218 | | доля | **1.0%** | | из них с кадастром (противоречие проверяемо вдвойне) | **21** | Живые примеры, все с екатеринбургским кадастровым округом `66:41`: ``` 9618 р-н Орджоникидзевский, мкр. Уралмаш 66:41:0106108:104 Екатеринбург + Нижний Тагил 372796 р-н Чкаловский, мкр. Химмаш 66:41:0509002:38 Екатеринбург + Каменск-Уральский 9701 р-н Орджоникидзевский, мкр. Эльмаш 66:41:0108001:381 Верхняя Пышма + Екатеринбург 372859 р-н Кировский, мкр. Втузгородок 66:41:0704024:29 Екатеринбург + Нижний Тагил 192337 Екатеринбург, улица Чекистов, 9 66:41:0702058:91 Екатеринбург + Первоуральск ``` ### Почему это независимое наблюдение `listings.city` заполняется **городом-целью развёртки** (какую площадку и какой город мы обходили), а не разбором адресной строки. Кадастровый округ дома приходит из внешнего справочника. Это два разных источника, и их противоречие — настоящее свидетельство, а не тавтология. Именно этого не хватало в #2690: там все кандидаты сводились к одной строке, поэтому усиление ключа отменяло защиту. Здесь — нет. ### Чем это отличается от уже измеренного В #2777 разброс мерился **географически** — расстояние между координатами объявлений одного дома. Тот замер видит только дома, у которых координаты есть, и не различает «геокодер промахнулся» от «сшиты разные здания». Признак «разные города при одном кадастре» **работает без координат** и различает эти случаи: город развёртки промахнуться не может, он задан на входе. ### Что предлагается — и чего НЕ предлагается **Не предлагаю** использовать это как ключ схлопывания: вывод #2690 остаётся в силе, склеивать дома по этому признаку нечем. **Предлагаю** использовать как **детектор**: 82 дома — готовый список кандидатов на разбор, из них 21 с двойным свидетельством. Это в разы уже, чем 906 домов с разбросом больше 250 метров, и точнее — среди них нет промахов геокодера. Отдельный вопрос — стоит ли **запрещать** привязку, когда город объявления противоречит уже привязанным. Это меняет поведение сопоставителя и требует замера: сколько привязок отклонится и сколько из них были верными. ### Поправка к формулировке из разбора #2583 Там сказано «18 областных лотов несут кадастры `66:41`». Проверил: кадастры у объявлений **не заполнены вообще** (0 из 99 167), они есть только у домов (2648 из 9461). Дефект существует, но он о другом — не лоты несут чужой кадастр, а **дом собирает лоты из разных городов**. Формулировка вводила бы в заблуждение при поиске.
lekss361 added the
bug
data
priority/p1
scope/db
tradein
labels 2026-08-16 10:25:19 +00:00
Author
Collaborator

Почему химеры живут после #2808: два механизма, и оба измерены

Проверил на проде 21.08 — пришёл сюда из #2996 (Находка 3) через ручку «дома не на месте» (#3010).

Сегодня: 78 домов-химер (объявления из разных городов), 24 с кадастром, 684 объявления; 9 610 домов с объявлениями всего. Из 78 у 51 второй город появился ПОСЛЕ мержа #2808 (10.08), у пяти — сегодня. Но это даты перепривязки, не рождения — и вот почему.

Механизм 1 — Tier 1 воспроизводит старую химеру вечно

Все десять «чужих» лотов сегодняшних дальних химер привязаны методом source_link, confidence 1.00 — Tier 1:

SELECT house_id FROM house_sources WHERE ext_source=:s AND ext_id=:e LIMIT 1
 return (house_id, 1.0, "source_exact")

Ни сверки расстояния при координатах (лот «Первоуральск, ул. Малышева,7» в 39.8 км от дома), ни сверки города развёртки — страж #2808 стоит в Tier 2a и до него просто не доходят. Связь, рождённая один раз старым кодом, переиспользуется при каждом скрейпе. За 7 суток: 20 697 привязок source_link, из них 156 дальше 5 км от дома, 16 дальше 30 км.

Механизм 2 — страж #2808 односторонний

_skip_oblast_alias = _resolved_city is not None and _resolved_city != EKB_CITY_TOKEN

Страж защищает не-ЕКБ карточку от ЕКБ-алиаса. Обратное — ЕКБ-карточка без координат бьёт в бескоординатный глобально-уникальный алиас дома другого города — проходит свободно. Живой пример, дом 380061 «Первомайская ул.,70» (Нижний Тагил):

алиас «первомайская улица 70»: fingerprint a069bee… = address_fingerprint(addr, None, None)  ← бескоординатный
лот 10461579 (avito, city=Екатеринбург): привязан 17.08 15:55:41 (fingerprint), геокодирован 18.08 03:23 — ПОСЛЕ

Карточка пришла без координат, resolve_city_token("Первомайская ул.,70") = None, город развёртки = ЕКБ → страж молчит → Tier 2a → дом Тагила. Координаты появились через полдня, и сейчас лот стоит в 125.9 км от своего дома.

Масштаб дыры: ЕКБ-лот → не-ЕКБ дом через fingerprint — 15 всего, 1 после #2808 (тот самый). Обратное направление (не-ЕКБ без гео → дом ЕКБ) после #28080: страж в свою сторону работает. Из 15 исторических 11 — «Верхняя Пышма»: «Уральских Рабочих, 4», «Машиностроителей, 6Б» — агломерация, где улица реально есть в обоих городах; это не всегда химера, и детектору это надо учитывать.

Три «чужих с гео» после #2808 — и только один из них химера

дом город дома лот км лот↔дом км дом↔свои лоты диагноз
Машиностроителей, 10 ЕКБ Пышма 0.0 0.0 граница развёртки — не химера
Малышева, 4 ЕКБ Первоуральск 0.0 19.8 дом не на месте (#3010), не сопоставитель
Первомайская, 70 Тагил ЕКБ 125.9 62.9 химера через механизм 2

Что из этого следует

  • Рождение новых химер почти остановлено (#2808): 1 случай за 11 дней, через односторонность стража. Остаток — 78 исторических, которые Tier 1 будет воспроизводить, пока их не разорвать.
  • Правка 1 (узкая, безопасная): сделать страж симметричным — бескоординатная карточка не принимает алиас, чей дом известно принадлежит другому городу. Города у houses нет (#2996, Находка 3); суррогат уже есть — преобладающий listings.city по привязанным лотам либо кадастровый округ (66:41 = ЕКБ). На 11 днях это закрыло бы 1 случай — цена нулевая, но и эффект мал.
  • Правка 2 (спорная): Tier 1 сверять город развёртки / расстояние перед переиспользованием связи. Именно это задача называет «требует замера: сколько привязок отклонится и сколько из них были верными» — 156 дальше 5 км за неделю, часть из них «дома не на месте» (чужие координаты у дома, а не у лота). Без замера ложных отклонений не кодирую.
  • Подчистка 78 — отдельно: детектор есть (этот запрос), но решение «разрывать ли связь и куда вешать лот» — за владельцем, как и в #3010.

Измеритель, который я сам отбросил: «62.3 % алиасов с отпечатком, не равным отпечатку дома» — не заражение, а норма (отпечаток строится из координат лота, не дома). Чтобы не вводил в заблуждение при следующем разборе.

## Почему химеры живут после #2808: два механизма, и оба измерены Проверил на проде 21.08 — пришёл сюда из #2996 (Находка 3) через ручку «дома не на месте» (#3010). **Сегодня: 78 домов-химер** (объявления из разных городов), 24 с кадастром, 684 объявления; 9 610 домов с объявлениями всего. Из 78 у **51 второй город появился ПОСЛЕ мержа #2808** (10.08), у пяти — сегодня. Но это даты перепривязки, не рождения — и вот почему. ### Механизм 1 — Tier 1 воспроизводит старую химеру вечно Все десять «чужих» лотов сегодняшних дальних химер привязаны методом `source_link`, confidence 1.00 — Tier 1: ```python SELECT house_id FROM house_sources WHERE ext_source=:s AND ext_id=:e LIMIT 1 → return (house_id, 1.0, "source_exact") ``` Ни сверки расстояния при координатах (лот «Первоуральск, ул. Малышева,7» в 39.8 км от дома), ни сверки города развёртки — страж #2808 стоит в Tier 2a и до него просто не доходят. Связь, рождённая один раз старым кодом, переиспользуется при каждом скрейпе. За 7 суток: 20 697 привязок `source_link`, из них **156 дальше 5 км от дома, 16 дальше 30 км**. ### Механизм 2 — страж #2808 односторонний ```python _skip_oblast_alias = _resolved_city is not None and _resolved_city != EKB_CITY_TOKEN ``` Страж защищает **не-ЕКБ** карточку от ЕКБ-алиаса. Обратное — **ЕКБ-карточка без координат бьёт в бескоординатный глобально-уникальный алиас дома другого города** — проходит свободно. Живой пример, дом 380061 «Первомайская ул.,70» (Нижний Тагил): ``` алиас «первомайская улица 70»: fingerprint a069bee… = address_fingerprint(addr, None, None) ← бескоординатный лот 10461579 (avito, city=Екатеринбург): привязан 17.08 15:55:41 (fingerprint), геокодирован 18.08 03:23 — ПОСЛЕ ``` Карточка пришла без координат, `resolve_city_token("Первомайская ул.,70")` = None, город развёртки = ЕКБ → страж молчит → Tier 2a → дом Тагила. Координаты появились через полдня, и сейчас лот стоит в 125.9 км от своего дома. **Масштаб дыры:** ЕКБ-лот → не-ЕКБ дом через `fingerprint` — 15 всего, **1 после #2808** (тот самый). Обратное направление (не-ЕКБ без гео → дом ЕКБ) после #2808 — **0**: страж в свою сторону работает. Из 15 исторических 11 — «Верхняя Пышма»: «Уральских Рабочих, 4», «Машиностроителей, 6Б» — агломерация, где улица реально есть в обоих городах; это не всегда химера, и детектору это надо учитывать. ### Три «чужих с гео» после #2808 — и только один из них химера | дом | город дома | лот | км лот↔дом | км дом↔свои лоты | диагноз | |---|---|---|---:|---:|---| | Машиностроителей, 10 | ЕКБ | Пышма | 0.0 | 0.0 | граница развёртки — не химера | | Малышева, 4 | ЕКБ | Первоуральск | 0.0 | **19.8** | **дом не на месте** (#3010), не сопоставитель | | Первомайская, 70 | Тагил | ЕКБ | **125.9** | 62.9 | химера через механизм 2 | ### Что из этого следует - **Рождение новых химер почти остановлено** (#2808): 1 случай за 11 дней, через односторонность стража. Остаток — 78 исторических, которые Tier 1 будет воспроизводить, пока их не разорвать. - **Правка 1 (узкая, безопасная): сделать страж симметричным** — бескоординатная карточка не принимает алиас, чей дом известно принадлежит другому городу. Города у `houses` нет (#2996, Находка 3); суррогат уже есть — преобладающий `listings.city` по привязанным лотам либо кадастровый округ (`66:41` = ЕКБ). На 11 днях это закрыло бы 1 случай — цена нулевая, но и эффект мал. - **Правка 2 (спорная): Tier 1 сверять город развёртки / расстояние** перед переиспользованием связи. Именно это задача называет «требует замера: сколько привязок отклонится и сколько из них были верными» — 156 дальше 5 км за неделю, часть из них «дома не на месте» (чужие координаты у дома, а не у лота). Без замера ложных отклонений не кодирую. - **Подчистка 78** — отдельно: детектор есть (этот запрос), но решение «разрывать ли связь и куда вешать лот» — за владельцем, как и в #3010. Измеритель, который я сам отбросил: «62.3 % алиасов с отпечатком, не равным отпечатку дома» — не заражение, а норма (отпечаток строится из координат **лота**, не дома). Чтобы не вводил в заблуждение при следующем разборе.
Author
Collaborator

Замер 27.08: страж по расстоянию «объявление ↔ координата дома» починил бы не то — 73% дальних привязок верны, а неверна точка самого дома

Собирался ставить в Tier 1 ровно тот страж, который напрашивается из механизма 1 (переиспользование связи без сверки расстояния). Прежде чем писать код, померил, сколько связей он отбракует. Результат отменил замысел.

Что видно, если мерить от координаты дома

73 814 привязок с координатами у обеих сторон; 553 дальше 30 км, 54 дома. Верх списка:

5065 км  avito  Екатеринбург  дом 12141 «ул. Лодыгина,15А»
         объявления: 56.845, 60.662   (Екатеринбург)
         дом:        43.119, 131.951  (ВЛАДИВОСТОК)

Дом «Лодыгина, 15А» есть и в Екатеринбурге, и во Владивостоке. Геокодер поставил дому владивостокскую точку, объявления при этом екатеринбургские и согласны между собой.

Разделение 54 домов на два разных дефекта

домов привязок
все объявления дома далеко от его точки → неверна точка ДОМА 25 404
часть объявлений далеко → сшиты чужие карточки (настоящая химера) 29 149

То есть 404 из 553 дальних привязок (73%) — правильные, а неверна координата дома. Страж, сверяющий приходящую карточку с houses.geom, отбраковал бы их и отправил в Tier 2/3 — с шансом наштамповать дубликаты домов. Он чинил бы геокодирование средствами сопоставителя, и чинил бы в минус.

Мерить надо от собратьев, а не от дома

Устойчивая мера — расстояние от объявления до среднего по ОСТАЛЬНЫМ объявлениям того же дома: она не зависит от того, куда геокодер поставил сам дом.

порог привязок домов
> 2 км 1 701
> 10 км 363 91
> 30 км 78

(считалось по 73 826 привязкам в домах с ≥2 объявлений с координатами; среднее берётся арифметически по lat/lon — на этих масштабах разница с настоящим центроидом ничтожна. NB: вариант через ST_Difference даёт 224 вместо 78 и он неверен — оператор выбрасывает ВСЕ совпадающие точки, поэтому у дома с пятью объявлениями в одной точке «остальные» схлопываются в пустоту и расстояние считается до выброса.)

Это и есть популяция настоящих химер: 78 привязок, а не 553.

Что это меняет в плане правки

  1. Страж Tier 1 — от собратьев. При попадании в house_sources сверять координату приходящей карточки со средним по уже привязанным объявлениям дома, а не с houses.geom. Порог 30 км ловит межгородские сшивки (Первоуральск 40 км, Каменск-Уральский 100, Нижний Тагил 130) и не трогает внутригородской разброс; 10 км ловил бы ещё и Верхнюю Пышму (15 км), но там уже возможен шум от объявлений, геокодированных в центроид города.
  2. Отказ Tier 1 — не бесплатный. Провал в Tier 2/3 может породить новый дом; это и есть желаемое «расщепить запись», но счётчик отказов надо логировать, иначе расщепление пойдёт молча.
  3. 25 домов с неверной точкой — отдельный дефект, к сопоставителю отношения не имеет: у них объявления согласны между собой, а точка дома взята геокодером у одноимённой улицы в другом городе. Это ближе к #2771 (перенос координат) и #3010 (ручка «дома не на месте»): у таких домов координату надо не переносить, а заменять средним по объявлениям, ровно потому что объявления согласны.

Чего замер не отменяет

Механизм 2 (односторонний страж #2808) остаётся как описан: ЕКБ-карточка без координат по-прежнему свободно бьёт в бескоординатный алиас дома другого города. Бескоординатные случаи ни один из порогов выше не видит по построению — там нечего мерить.

Числа по химерам «разные города» держатся ровно те же, что 21.08: 78 домов из 9 257 с объявлениями.

## Замер 27.08: страж по расстоянию «объявление ↔ координата дома» починил бы не то — 73% дальних привязок верны, а неверна точка самого дома Собирался ставить в Tier 1 ровно тот страж, который напрашивается из механизма 1 (переиспользование связи без сверки расстояния). Прежде чем писать код, померил, сколько связей он отбракует. Результат отменил замысел. ### Что видно, если мерить от координаты дома 73 814 привязок с координатами у обеих сторон; **553 дальше 30 км**, 54 дома. Верх списка: ``` 5065 км avito Екатеринбург дом 12141 «ул. Лодыгина,15А» объявления: 56.845, 60.662 (Екатеринбург) дом: 43.119, 131.951 (ВЛАДИВОСТОК) ``` Дом «Лодыгина, 15А» есть и в Екатеринбурге, и во Владивостоке. Геокодер поставил дому владивостокскую точку, объявления при этом екатеринбургские и согласны между собой. ### Разделение 54 домов на два разных дефекта | | домов | привязок | |---|---:|---:| | **все** объявления дома далеко от его точки → неверна точка ДОМА | **25** | **404** | | **часть** объявлений далеко → сшиты чужие карточки (настоящая химера) | 29 | 149 | То есть **404 из 553 дальних привязок (73%) — правильные**, а неверна координата дома. Страж, сверяющий приходящую карточку с `houses.geom`, отбраковал бы их и отправил в Tier 2/3 — с шансом наштамповать дубликаты домов. Он чинил бы геокодирование средствами сопоставителя, и чинил бы в минус. ### Мерить надо от собратьев, а не от дома Устойчивая мера — расстояние от объявления до среднего по ОСТАЛЬНЫМ объявлениям того же дома: она не зависит от того, куда геокодер поставил сам дом. | порог | привязок | домов | |---|---:|---:| | > 2 км | 1 701 | — | | > 10 км | 363 | 91 | | > 30 км | **78** | — | (считалось по 73 826 привязкам в домах с ≥2 объявлений с координатами; среднее берётся арифметически по lat/lon — на этих масштабах разница с настоящим центроидом ничтожна. NB: вариант через `ST_Difference` даёт 224 вместо 78 и он неверен — оператор выбрасывает ВСЕ совпадающие точки, поэтому у дома с пятью объявлениями в одной точке «остальные» схлопываются в пустоту и расстояние считается до выброса.) Это и есть популяция настоящих химер: **78 привязок**, а не 553. ### Что это меняет в плане правки 1. **Страж Tier 1 — от собратьев.** При попадании в `house_sources` сверять координату приходящей карточки со средним по уже привязанным объявлениям дома, а не с `houses.geom`. Порог 30 км ловит межгородские сшивки (Первоуральск 40 км, Каменск-Уральский 100, Нижний Тагил 130) и не трогает внутригородской разброс; 10 км ловил бы ещё и Верхнюю Пышму (15 км), но там уже возможен шум от объявлений, геокодированных в центроид города. 2. **Отказ Tier 1 — не бесплатный.** Провал в Tier 2/3 может породить новый дом; это и есть желаемое «расщепить запись», но счётчик отказов надо логировать, иначе расщепление пойдёт молча. 3. **25 домов с неверной точкой — отдельный дефект**, к сопоставителю отношения не имеет: у них объявления согласны между собой, а точка дома взята геокодером у одноимённой улицы в другом городе. Это ближе к #2771 (перенос координат) и #3010 (ручка «дома не на месте»): у таких домов координату надо не переносить, а **заменять** средним по объявлениям, ровно потому что объявления согласны. ### Чего замер не отменяет Механизм 2 (односторонний страж #2808) остаётся как описан: ЕКБ-карточка без координат по-прежнему свободно бьёт в бескоординатный алиас дома другого города. Бескоординатные случаи ни один из порогов выше не видит по построению — там нечего мерить. Числа по химерам «разные города» держатся ровно те же, что 21.08: **78 домов** из 9 257 с объявлениями.
Sign in to join this conversation.
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#2777
No description provided.