feat(estimator): exclude city-centroid listings from radius analogs (Refs #769 Part E) #811

Merged
lekss361 merged 1 commit from fix/769-e-geo-centroid into main 2026-05-30 18:26:40 +00:00
Collaborator

Что

Последняя часть #769 (A1/A2/B/C/D — в #798/#804). Finding #17: bare-city адреса («Екатеринбург (Cian/N1)» без дома) геокодились в city-центроид и сохранялись как точные lat/lon → mislocated листинги участвовали в radius ST_DWithin-аналогах, загрязняя comp set оценки.

Как

  • 089 миграция: ALTER TABLE listings ADD COLUMN IF NOT EXISTS geo_precision text (idempotent, BEGIN/COMMIT; backfill существующих city-coords — ops follow-up).
  • ScrapedLot.geo_precision + проброс в save_listings.
  • geocode_missing.py: geo_precision='city' через существующий _geocode_is_coarse (детерминированно: confidence=locality / нет house-number в full_address).
  • estimator._fetch_analogs Tier H + Tier W: + AND (geo_precision IS DISTINCT FROM 'city').

Консервативность (важно — client-input-adjacent)

IS DISTINCT FROM 'city' → строки с geo_precision IS NULL проходят. Т.е. до backfill существующие листинги (NULL) продолжают участвовать → сдвига оценок на merge нет; исключаются только новые/перегеокоженные city-centroid'ы. Тронут только WHERE-фильтр radius (2 строки) — pricing/anchor/median НЕ изменены.

Тесты

tests/test_geo_precision.py (7): city-fallback→geo_precision='city'; radius-query содержит фильтр. pytest -k "geo or geocode or estimator or radius"270 passed, ruff clean.

Refs #769 (закрывает все 5 частей: A1/A2/B/C/D/E)

## Что Последняя часть #769 (A1/A2/B/C/D — в #798/#804). Finding #17: bare-city адреса («Екатеринбург (Cian/N1)» без дома) геокодились в **city-центроид** и сохранялись как точные lat/lon → mislocated листинги участвовали в radius `ST_DWithin`-аналогах, загрязняя comp set оценки. ## Как - **089** миграция: `ALTER TABLE listings ADD COLUMN IF NOT EXISTS geo_precision text` (idempotent, BEGIN/COMMIT; backfill существующих city-coords — ops follow-up). - `ScrapedLot.geo_precision` + проброс в `save_listings`. - `geocode_missing.py`: `geo_precision='city'` через **существующий** `_geocode_is_coarse` (детерминированно: confidence=locality / нет house-number в `full_address`). - `estimator._fetch_analogs` Tier H + Tier W: `+ AND (geo_precision IS DISTINCT FROM 'city')`. ## Консервативность (важно — client-input-adjacent) `IS DISTINCT FROM 'city'` → строки с `geo_precision IS NULL` **проходят**. Т.е. до backfill существующие листинги (NULL) продолжают участвовать → **сдвига оценок на merge нет**; исключаются только новые/перегеокоженные city-centroid'ы. Тронут **только** WHERE-фильтр radius (2 строки) — pricing/anchor/median НЕ изменены. ## Тесты `tests/test_geo_precision.py` (7): city-fallback→`geo_precision='city'`; radius-query содержит фильтр. `pytest -k "geo or geocode or estimator or radius"` → **270 passed**, ruff clean. Refs #769 (закрывает все 5 частей: A1/A2/B/C/D/E)
bot-backend added 1 commit 2026-05-30 18:22:42 +00:00
Закрывает последнюю часть #769 (A1/A2/B/C/D в #798/#804). Finding #17:
bare-city адреса геокодились в city-центроид и сохранялись как точные lat/lon →
mislocated листинги участвовали в radius ST_DWithin-аналогах.

- 089: ALTER TABLE listings ADD COLUMN IF NOT EXISTS geo_precision text (idempot).
- ScrapedLot.geo_precision + проброс в save_listings.
- geocode_missing.py: geo_precision='city' через существующий _geocode_is_coarse.
- estimator _fetch_analogs Tier H+W: + AND (geo_precision IS DISTINCT FROM 'city').
  NULL проходит (консервативно, сдвига до backfill нет). Pricing не тронут.

7 тестов; 270 passed, ruff clean.
lekss361 merged commit c73a79ee6f into main 2026-05-30 18:26:40 +00:00
lekss361 deleted branch fix/769-e-geo-centroid 2026-05-30 18:26:40 +00:00
bot-reviewer approved these changes 2026-05-30 18:27:09 +00:00
bot-reviewer left a comment
Collaborator

APPROVE#769 Part E, корректный консервативный фикс radius-comp-set.

Проверено:

  • _geocode_is_coarse(geo: GeocodeResult) существует (estimator.py:1322), сигнатура/логика точно совпадают с использованием в geocode_missing.py (confidence=='locality' OR нет house-number токена → coarse). Импорт валиден.
  • Нет циркулярного импорта: estimator.py не импортирует app.tasks/geocode_missing — связь однонаправленная.
  • Скоуп Part E соблюдён: тронут только WHERE-фильтр comp-set в _fetch_analogs (Tier H + Tier W, 2 строки). IS DISTINCT FROM 'city' пропускает NULL → сдвига оценок на merge нет (существующие листинги участвуют). Pricing/anchor/median/guard-bands НЕ изменены (#755 соблюдён).
  • 089 идемпотентна (ADD COLUMN IF NOT EXISTS, BEGIN/COMMIT, после 088). ON CONFLICT не сбрасывает geo_precision на ре-скрейпе (сохраняется backfill-значение).
  • Тесты покрывают обе ветки (city/None + locality-confidence) + наличие SQL-фильтра в обоих tier'ах.

Anti-regression: self-extending файлы не затронуты, секретов нет, SQL-фильтр — статический литерал (без injection).

Минор (не блокер): импорт приватной _geocode_is_coarse через модульную границу — слегка inelegant, но дублирование coarse-логики хуже. ОК.

Post-deploy: targeted backfill известных city-centroid адресов (UPDATE listings SET geo_precision='city' WHERE address IN (...)) — ops follow-up по комменту в 089.

<!-- gendesign-review-bot: sha=b736d7b verdict=approve --> ✅ **APPROVE** — #769 Part E, корректный консервативный фикс radius-comp-set. **Проверено:** - `_geocode_is_coarse(geo: GeocodeResult)` существует (`estimator.py:1322`), сигнатура/логика точно совпадают с использованием в `geocode_missing.py` (confidence=='locality' OR нет house-number токена → coarse). Импорт валиден. - **Нет циркулярного импорта**: `estimator.py` не импортирует `app.tasks`/`geocode_missing` — связь однонаправленная. - Скоуп Part E соблюдён: тронут **только** WHERE-фильтр comp-set в `_fetch_analogs` (Tier H + Tier W, 2 строки). `IS DISTINCT FROM 'city'` пропускает NULL → **сдвига оценок на merge нет** (существующие листинги участвуют). Pricing/anchor/median/guard-bands НЕ изменены (#755 соблюдён). - `089` идемпотентна (`ADD COLUMN IF NOT EXISTS`, BEGIN/COMMIT, после 088). ON CONFLICT не сбрасывает `geo_precision` на ре-скрейпе (сохраняется backfill-значение). - Тесты покрывают обе ветки (city/None + locality-confidence) + наличие SQL-фильтра в обоих tier'ах. **Anti-regression:** self-extending файлы не затронуты, секретов нет, SQL-фильтр — статический литерал (без injection). **Минор (не блокер):** импорт приватной `_geocode_is_coarse` через модульную границу — слегка inelegant, но дублирование coarse-логики хуже. ОК. Post-deploy: targeted backfill известных city-centroid адресов (`UPDATE listings SET geo_precision='city' WHERE address IN (...)`) — ops follow-up по комменту в 089.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#811
No description provided.