[CRITICAL] tradein: скрапер не пишет город в адрес объявления — данные области уходят в Екатеринбург #2594

Closed
opened 2026-07-31 18:03:05 +00:00 by lekss361 · 2 comments
Owner

Найдено 2026-07-31 при проверке вопроса владельца «неужели по другим городам так мало объявлений». Объявлений собирается достаточно — они не привязываются к городу.

Факты

Сегодняшний сбор Авито (243 объявления, все активны):

Показатель Значение
всего собрано 243
с координатами 12
город в тексте адреса 5

Примеры адресов как они приходят: пр-т Вагоностроителей,70, ул. Дружинина,33, Уральский пр-т,111, ул. Победы,30, Ул. Тенистая, стр. 2А. Это улицы Нижнего Тагила — без названия города.

Почему это критично

Улицы с такими же названиями есть в екатеринбургском реестре домов:

ленина     — 112 домов
победы     —  89
тенистая   —  17

Тагильское объявление «ул. Победы, 30» без города при геокодировании попадает в путь «город не назван» → защита (#2582, только что смерженная) его пропускает как легитимный екатеринбургский адрес → объявление получает координаты Екатеринбурга.

Последствия двойные:

  1. объявление становится «аналогом» в екатеринбургских оценках и тянет медиану;
  2. в своём городе оно исчезает из выборки по радиусу — то есть оценка по Тагилу строится без него.

Это тот же класс дефекта, что чинился в #2582/#2580, но с другой стороны: не пользовательский ввод, а адрес объявления из скрапера.

Корень

Развёртка запускается отдельно по каждому городу (avito_city_sweep_nizhniy_tagil и т.д., по слагу площадки — CITY_LOCATIONS в orchestration/pipeline.py). То есть в момент сбора город известен точно и достоверно. Но в адрес объявления он не записывается, и дальше система вынуждена угадывать его по тексту — а текста нет.

Что сделать

Проставлять город при сборе из контекста развёртки, а не выводить потом из адреса. Варианты (выбрать при проработке): дописывать город в address при записи; либо отдельная колонка city в listings с заполнением из city-sweep — надёжнее и не портит исходный текст.

Учесть:

  • у объявлений из общегородской развёртки Екатеринбурга город тоже должен проставляться, иначе асимметрия;
  • уже накопленные строки (384 по Тагилу, 267 по Каменску-Уральскому, 114 по Первоуральску) — отдельный бэкфилл: город восстанавливается по тому, какая развёртка их принесла (scrape_runs / listing_sources);
  • 231 из 243 сегодняшних объявлений без координат — они ждут ночного геокодирования, которое как раз и снапнет их в Екатеринбург, если город не проставить раньше.

Проверка

После фикса: объявления, собранные развёрткой Тагила, имеют город «Нижний Тагил»; их координаты попадают в границы Тагила, а не Екатеринбурга; оценка по тагильскому адресу видит их как аналоги. Контроль: доля объявлений без города в адресе по каждому источнику должна упасть до нуля для городских развёрток.

Связано: #2576 (эпик область), #2582, #2583.

Найдено 2026-07-31 при проверке вопроса владельца «неужели по другим городам так мало объявлений». Объявлений собирается достаточно — они **не привязываются к городу**. ## Факты Сегодняшний сбор Авито (243 объявления, все активны): | Показатель | Значение | |---|---| | всего собрано | 243 | | с координатами | **12** | | город в тексте адреса | **5** | Примеры адресов как они приходят: `пр-т Вагоностроителей,70`, `ул. Дружинина,33`, `Уральский пр-т,111`, `ул. Победы,30`, `Ул. Тенистая, стр. 2А`. Это улицы Нижнего Тагила — **без названия города**. ## Почему это критично Улицы с такими же названиями есть в екатеринбургском реестре домов: ``` ленина — 112 домов победы — 89 тенистая — 17 ``` Тагильское объявление «ул. Победы, 30» без города при геокодировании попадает в путь «город не назван» → защита (#2582, только что смерженная) его пропускает как легитимный екатеринбургский адрес → объявление получает **координаты Екатеринбурга**. Последствия двойные: 1. объявление становится «аналогом» в екатеринбургских оценках и тянет медиану; 2. в своём городе оно исчезает из выборки по радиусу — то есть оценка по Тагилу строится без него. Это тот же класс дефекта, что чинился в #2582/#2580, но с другой стороны: не пользовательский ввод, а **адрес объявления из скрапера**. ## Корень Развёртка запускается **отдельно по каждому городу** (`avito_city_sweep_nizhniy_tagil` и т.д., по слагу площадки — `CITY_LOCATIONS` в `orchestration/pipeline.py`). То есть в момент сбора город известен точно и достоверно. Но в адрес объявления он не записывается, и дальше система вынуждена угадывать его по тексту — а текста нет. ## Что сделать Проставлять город при сборе из контекста развёртки, а не выводить потом из адреса. Варианты (выбрать при проработке): дописывать город в `address` при записи; либо отдельная колонка `city` в `listings` с заполнением из city-sweep — надёжнее и не портит исходный текст. Учесть: - у объявлений из общегородской развёртки Екатеринбурга город тоже должен проставляться, иначе асимметрия; - уже накопленные строки (384 по Тагилу, 267 по Каменску-Уральскому, 114 по Первоуральску) — отдельный бэкфилл: город восстанавливается по тому, какая развёртка их принесла (`scrape_runs` / `listing_sources`); - 231 из 243 сегодняшних объявлений **без координат** — они ждут ночного геокодирования, которое как раз и снапнет их в Екатеринбург, если город не проставить раньше. ## Проверка После фикса: объявления, собранные развёрткой Тагила, имеют город «Нижний Тагил»; их координаты попадают в границы Тагила, а не Екатеринбурга; оценка по тагильскому адресу видит их как аналоги. Контроль: доля объявлений без города в адресе по каждому источнику должна упасть до нуля для городских развёрток. Связано: #2576 (эпик область), #2582, #2583.
lekss361 reopened this issue 2026-07-31 20:17:26 +00:00
Author
Owner

Переоткрыто: PR #2598 закрыл только шаг записи (колонка listings.city заполняется из контекста развёртки), но петля не замкнута — геокодирование новую колонку не читает.

Смержено (#2598, da329cda): миграция 196, save_listings(..., city=...) с COALESCE(EXCLUDED.city, listings.city), resolve_city_name() проброшен в городские развёртки Авито/Яндекса/Циана. Глубокое ревью с эмпирическим репро: 19 новых тестов падают на до-PR коде, 275 passed в смежных модулях. Проверено отдельно: radius_m 25 км у областных яндекс-развёрток инертен (провайдер ходит по rgid), поэтому Пышма не тянет ЕКБ.

Что осталось

1. Замкнуть петлю (caller-side, geocoder.py не трогать — переработан дважды). Мест три, не одно:

  • app/tasks/geocode_missing.py:124 — сейчас дедуп «1 адрес → 1 вызов» по одному тексту адреса; нужна группировка по паре (address, city) и передача city_hint в geocode() (сигнатура уже принимает, geocoder.py:1168). UPDATE тоже по паре, через IS NOT DISTINCT FROM — иначе один тагильский адрес перезапишет одноимённый екатеринбургский.
  • app/tasks/backfill_listings_coords_geoportal.py — городской гейт H3 сейчас только по ТЕКСТУ адреса (_names_non_ekb_city). Голый «ул. Победы, 30» гейт проходит и может сматчиться с ЕКБ-домом из ekb_geoportal_buildings. Добавить гейт по колонке: city IS NOT NULL AND city <> 'Екатеринбург' → skip. Окно этой задачи идёт ПЕРЕД geocode_missing, так что без гейта она успевает испортить координаты первой.
  • app/api/v1/admin.py:313 — per-ID геокод-endpoint зовёт geocode() без city_hint.

2. Бэкфилл накопленных. Числа из тела issue не воспроизводятся: scraped_at обновляется при каждом повторном увидении (base.py, upsert SET scraped_at = NOW()), поэтому корреляция «объявление ↔ окно прогона» ненадёжна при параллельных джобах одного источника. Развёрток по Первоуральску/Серову/Пышме не было вовсе. Фактическая атрибуция per-listing бедная: raw_payload без города у всех четырёх источников, avito_location_id NULL у 231 из 243. Рабочие каналы: слаг в URL Авито (avito.ru/nizhniy_tagil/… — 155 без координат, детерминированно), текст адреса (yandex: Тагил 279, Каменск 69, cian: Тагил 22). Остаток пересчитать после того, как ежедневные развёртки сами проставят город живым объявлениям через COALESCE.

3. Мелочи из ревью (не блокеры). tests/test_city_sweep.py:44 — guard сверяет только CITY_LOCATIONS, город, добавленный лишь в CITY_ANCHORS, получит штамп «Екатеринбург»; починка в одно слово. Ветка else None для домклика (pipeline.py:3575) без теста — покрыть при выкатке B2.

Потребителей listings.city пока нет ни одного (grep) — колонка write-only, ошибки разметки инертны до появления первого читателя.

Переоткрыто: PR #2598 закрыл только шаг записи (колонка `listings.city` заполняется из контекста развёртки), но **петля не замкнута** — геокодирование новую колонку не читает. Смержено (#2598, `da329cda`): миграция 196, `save_listings(..., city=...)` с `COALESCE(EXCLUDED.city, listings.city)`, `resolve_city_name()` проброшен в городские развёртки Авито/Яндекса/Циана. Глубокое ревью с эмпирическим репро: 19 новых тестов падают на до-PR коде, 275 passed в смежных модулях. Проверено отдельно: `radius_m` 25 км у областных яндекс-развёрток инертен (провайдер ходит по rgid), поэтому Пышма не тянет ЕКБ. ## Что осталось **1. Замкнуть петлю (caller-side, `geocoder.py` не трогать — переработан дважды).** Мест три, не одно: - `app/tasks/geocode_missing.py:124` — сейчас дедуп «1 адрес → 1 вызов» по одному тексту адреса; нужна группировка по паре `(address, city)` и передача `city_hint` в `geocode()` (сигнатура уже принимает, `geocoder.py:1168`). UPDATE тоже по паре, через `IS NOT DISTINCT FROM` — иначе один тагильский адрес перезапишет одноимённый екатеринбургский. - `app/tasks/backfill_listings_coords_geoportal.py` — городской гейт H3 сейчас только по ТЕКСТУ адреса (`_names_non_ekb_city`). Голый «ул. Победы, 30» гейт проходит и может сматчиться с ЕКБ-домом из `ekb_geoportal_buildings`. Добавить гейт по колонке: `city IS NOT NULL AND city <> 'Екатеринбург'` → skip. Окно этой задачи идёт ПЕРЕД geocode_missing, так что без гейта она успевает испортить координаты первой. - `app/api/v1/admin.py:313` — per-ID геокод-endpoint зовёт `geocode()` без `city_hint`. **2. Бэкфилл накопленных.** Числа из тела issue не воспроизводятся: `scraped_at` обновляется при каждом повторном увидении (`base.py`, upsert `SET scraped_at = NOW()`), поэтому корреляция «объявление ↔ окно прогона» ненадёжна при параллельных джобах одного источника. Развёрток по Первоуральску/Серову/Пышме не было вовсе. Фактическая атрибуция per-listing бедная: `raw_payload` без города у всех четырёх источников, `avito_location_id` NULL у 231 из 243. Рабочие каналы: слаг в URL Авито (`avito.ru/nizhniy_tagil/…` — 155 без координат, детерминированно), текст адреса (yandex: Тагил 279, Каменск 69, cian: Тагил 22). Остаток пересчитать после того, как ежедневные развёртки сами проставят город живым объявлениям через COALESCE. **3. Мелочи из ревью (не блокеры).** `tests/test_city_sweep.py:44` — guard сверяет только `CITY_LOCATIONS`, город, добавленный лишь в `CITY_ANCHORS`, получит штамп «Екатеринбург»; починка в одно слово. Ветка `else None` для домклика (`pipeline.py:3575`) без теста — покрыть при выкатке B2. Потребителей `listings.city` пока нет ни одного (grep) — колонка write-only, ошибки разметки инертны до появления первого читателя.
Author
Owner

Закрываю — цепочка замкнута и проверена на живых данных.

Все три шага в проде: город проставляется при сборе (#2598), доходит до геокодирования подсказкой (#2601), восстановлен задним числом у накопленных объявлений (#2606). Плюс попутно расчищена очередь геокодирования (#2605, issue #2604) — без этого механизм простаивал и проверить цепочку было не на чем.

Доказательство: первый ночной прогон после всех фиксов

geocode_missing_listings id=2886, 2026-08-01 13:18 UTC:

 id  | status |     started (UTC)     | checked | saved | skipped
2886 | done   | 2026-08-01 13:18:39   |   229   |  393  |   98    ← после фиксов
2755 | done   | 2026-07-31 01:35:35   |   592   |   0   |  592
2690 | done   | 2026-07-30 04:53:25   |   377   |   0   |  377
2666 | done   | 2026-07-29 22:20:03   |   400   |   0   |  400

393 объявления получили координаты — первый ненулевой результат после восьми ночей подряд с честным нулём. (checked — адреса, saved — объявления: одна пара «адрес + город» обновляет несколько строк.)

Город соблюдён

city всего активных с координатами avg lat avg lon
Нижний Тагил 170 119 57.913 60.008
Екатеринбург 599 404 56.792 60.599

Тагильские объявления сели в Тагил, екатеринбургские — в Екатеринбург.

Контрольная проверка на исходный баг: объявлений с городом «Нижний Тагил» и координатами внутри рамки Екатеринбурга (lat 56.7-57.0, lon 60.4-60.8) — 0.

То есть сценарий из тела issue («ул. Победы, 30» без города → координаты ЕКБ → тянет чужую медиану и исчезает из своей выборки) больше не воспроизводится.

Хвосты вынесены отдельно

  • #2603 — непрошитый двойник в scripts/geocode_deals_nominatim.py (та же форма бага по таблице deals, где город заполнен на 100%) + наблюдаемость городского гейта.
  • #2604 п.2 — region_code = 66 у 16912 объявлений чужих регионов; ждёт решения владельца, влияния на оценки нет (все неактивны).
  • #2607 — соседняя молча неработающая ночная задача, найдена по ходу.
✅ **Закрываю — цепочка замкнута и проверена на живых данных.** Все три шага в проде: город проставляется при сборе (#2598), доходит до геокодирования подсказкой (#2601), восстановлен задним числом у накопленных объявлений (#2606). Плюс попутно расчищена очередь геокодирования (#2605, issue #2604) — без этого механизм простаивал и проверить цепочку было не на чем. ## Доказательство: первый ночной прогон после всех фиксов `geocode_missing_listings` id=2886, 2026-08-01 13:18 UTC: ``` id | status | started (UTC) | checked | saved | skipped 2886 | done | 2026-08-01 13:18:39 | 229 | 393 | 98 ← после фиксов 2755 | done | 2026-07-31 01:35:35 | 592 | 0 | 592 2690 | done | 2026-07-30 04:53:25 | 377 | 0 | 377 2666 | done | 2026-07-29 22:20:03 | 400 | 0 | 400 ``` **393 объявления получили координаты** — первый ненулевой результат после восьми ночей подряд с честным нулём. (`checked` — адреса, `saved` — объявления: одна пара «адрес + город» обновляет несколько строк.) ## Город соблюдён | city | всего активных | с координатами | avg lat | avg lon | |---|---|---|---|---| | Нижний Тагил | 170 | 119 | **57.913** | **60.008** | | Екатеринбург | 599 | 404 | **56.792** | **60.599** | Тагильские объявления сели в Тагил, екатеринбургские — в Екатеринбург. **Контрольная проверка на исходный баг:** объявлений с городом «Нижний Тагил» и координатами внутри рамки Екатеринбурга (lat 56.7-57.0, lon 60.4-60.8) — **0**. То есть сценарий из тела issue («ул. Победы, 30» без города → координаты ЕКБ → тянет чужую медиану и исчезает из своей выборки) больше не воспроизводится. ## Хвосты вынесены отдельно - #2603 — непрошитый двойник в `scripts/geocode_deals_nominatim.py` (та же форма бага по таблице `deals`, где город заполнен на 100%) + наблюдаемость городского гейта. - #2604 п.2 — `region_code = 66` у 16912 объявлений чужих регионов; ждёт решения владельца, влияния на оценки нет (все неактивны). - #2607 — соседняя молча неработающая ночная задача, найдена по ходу.
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#2594
No description provided.