feat(tradein): DaData /clean/address backfill для houses (PR Q3) #606

Merged
Light1YT merged 1 commit from feat/tradein-dadata-house-backfill into main 2026-05-27 14:43:52 +00:00
Owner

Summary

Закрывает Q-серию по DaData. После Q1 (#604, on-demand enrich /estimate) + Q2 (#605, /suggest tier) — теперь batch enrichment всего houses через DaData /clean/address.

Зачем

Из 12,437 houses на проде:

  • 3,801 — lat IS NULL OR lon IS NULL (нет coords)
  • 12,437 — cadastral_number IS NULL (100%, у Yandex нет cadnum)

DaData /clean/address возвращает coords + house_cadnum + house_fias_id + qc-коды. На demo квоте 100 req/день → ~124 дня walltime для полного backfill, бесплатно.

Why this matters: с house_fias_id на каждом доме можно делать deterministic house-match между listings и target адресом (Tier S — same building), значительно улучшая качество аналогов (#6 House-match приоритет).

Changes

File LOC Type
backend/data/sql/070_houses_dadata_enrichment.sql 62 new migration
backend/scripts/backfill_houses_dadata.py 518 new script
backend/tests/scripts/test_backfill_houses_dadata.py 464 new tests

3 файла, +1044 LOC.

Migration 070

4 nullable columns + 3 partial indexes (IF NOT EXISTS everywhere, idempotent):

  • house_fias_id text (+ partial idx)
  • cadastral_number (был, +partial idx)
  • dadata_qc_geo integer
  • dadata_qc_house integer
  • dadata_enriched_at timestamptz (+ partial idx WHERE IS NULL — для resume-safe selection)

Script logic

Priority:
  1. coords  — NULL lat/lon (3801 rows)
  2. cadnum  — NULL cadastral_number, coords есть (~8636 rows)
  3. both    — обе очереди (default)

Per-row:
  - SAVEPOINT (db.begin_nested) — один fail не ломает batch
  - clean_address(house.address или COALESCE с full_address)
  - if qc_geo IN (0,1): UPDATE lat/lon/cadnum/fias/qc + enriched_at
  - else (qc=2,3,4,None): UPDATE только qc + enriched_at (tracking, без overwrite)
  - sleep 0.5s между запросами

Final log: processed / enriched / no_change / failed / skipped + by_priority breakdown.

Flags: --limit N (default 100), --batch LABEL, --dry-run, --priority coords|cadnum|both.

Resume-safe: WHERE dadata_enriched_at IS NULL → следующий запуск пропускает уже обработанные.

Tests

tests/scripts/test_backfill_houses_dadata.py — 20 passed
existing tests/test_backfill_house_coords.py — 20 passed (no regression)
existing tests/scripts/test_backfill_listing_sources.py — 19 passed (no regression)

Combined: 59 passed in 0.40s

Покрывает:

  • _is_enriched_result accept/reject (qc_geo 0/1/2/3, None, no-coords)
  • bind-shape для enriched + attempt-only UPDATE
  • _run_backfill: qc=0 happy, qc=3 tracking, None result, exception isolation, --dry-run
  • _select_candidates: priority filtering
  • --limit cap
  • Missing creds → SystemExit fail-fast
  • by_priority bucket stats

Ruff clean. Все DaData API mocked — нет реальных запросов.

Deploy plan

  1. Merge → migration 070 применяется автоматически на VPS через deploy.yml
  2. Verify columns на проде
  3. Запустить скрипт первый раз вручную --limit 50 --priority coords --dry-run чтобы убедиться что candidates находит
  4. Реальный первый запуск --limit 100 --priority coords (заполнит ~100 coords)
  5. Поставить на cron (cron в /etc/cron.daily/ или Celery beat) — daily 100/день
  6. Мониторить dadata_enriched_at в DB — через 38 дней coords готовы, через 124 дня вся база

Follow-ups (не блокеры)

  • После полной backfill — использовать houses.house_fias_id в _fetch_analogs как Tier S (same building exact match) — открывает дорогу к #6 House-match priorities
  • Cron config commit в .forgejo/workflows/ (отдельный PR, после первого manual test)
## Summary Закрывает Q-серию по DaData. После Q1 (#604, on-demand enrich /estimate) + Q2 (#605, /suggest tier) — теперь **batch enrichment всего houses** через DaData /clean/address. ## Зачем Из 12,437 houses на проде: - 3,801 — `lat IS NULL OR lon IS NULL` (нет coords) - 12,437 — `cadastral_number IS NULL` (100%, у Yandex нет cadnum) DaData /clean/address возвращает coords + `house_cadnum` + `house_fias_id` + qc-коды. На demo квоте 100 req/день → ~124 дня walltime для полного backfill, бесплатно. **Why this matters**: с `house_fias_id` на каждом доме можно делать **deterministic house-match** между listings и target адресом (Tier S — same building), значительно улучшая качество аналогов (#6 House-match приоритет). ## Changes | File | LOC | Type | |---|---|---| | `backend/data/sql/070_houses_dadata_enrichment.sql` | 62 | new migration | | `backend/scripts/backfill_houses_dadata.py` | 518 | new script | | `backend/tests/scripts/test_backfill_houses_dadata.py` | 464 | new tests | **3 файла, +1044 LOC.** ## Migration 070 4 nullable columns + 3 partial indexes (`IF NOT EXISTS` everywhere, idempotent): - `house_fias_id text` (+ partial idx) - `cadastral_number` (был, +partial idx) - `dadata_qc_geo integer` - `dadata_qc_house integer` - `dadata_enriched_at timestamptz` (+ partial idx WHERE IS NULL — для resume-safe selection) ## Script logic ``` Priority: 1. coords — NULL lat/lon (3801 rows) 2. cadnum — NULL cadastral_number, coords есть (~8636 rows) 3. both — обе очереди (default) Per-row: - SAVEPOINT (db.begin_nested) — один fail не ломает batch - clean_address(house.address или COALESCE с full_address) - if qc_geo IN (0,1): UPDATE lat/lon/cadnum/fias/qc + enriched_at - else (qc=2,3,4,None): UPDATE только qc + enriched_at (tracking, без overwrite) - sleep 0.5s между запросами Final log: processed / enriched / no_change / failed / skipped + by_priority breakdown. ``` Flags: `--limit N` (default 100), `--batch LABEL`, `--dry-run`, `--priority coords|cadnum|both`. Resume-safe: WHERE `dadata_enriched_at IS NULL` → следующий запуск пропускает уже обработанные. ## Tests ``` tests/scripts/test_backfill_houses_dadata.py — 20 passed existing tests/test_backfill_house_coords.py — 20 passed (no regression) existing tests/scripts/test_backfill_listing_sources.py — 19 passed (no regression) Combined: 59 passed in 0.40s ``` Покрывает: - `_is_enriched_result` accept/reject (qc_geo 0/1/2/3, None, no-coords) - bind-shape для enriched + attempt-only UPDATE - `_run_backfill`: qc=0 happy, qc=3 tracking, None result, exception isolation, --dry-run - `_select_candidates`: priority filtering - --limit cap - Missing creds → SystemExit fail-fast - by_priority bucket stats Ruff clean. Все DaData API mocked — нет реальных запросов. ## Deploy plan 1. Merge → migration 070 применяется автоматически на VPS через deploy.yml 2. Verify columns на проде 3. **Запустить** скрипт первый раз вручную `--limit 50 --priority coords --dry-run` чтобы убедиться что candidates находит 4. **Реальный первый запуск** `--limit 100 --priority coords` (заполнит ~100 coords) 5. Поставить на cron (cron в `/etc/cron.daily/` или Celery beat) — daily 100/день 6. Мониторить `dadata_enriched_at` в DB — через 38 дней coords готовы, через 124 дня вся база ## Follow-ups (не блокеры) - После полной backfill — использовать `houses.house_fias_id` в `_fetch_analogs` как Tier S (same building exact match) — открывает дорогу к #6 House-match priorities - Cron config commit в `.forgejo/workflows/` (отдельный PR, после первого manual test)
Light1YT added 1 commit 2026-05-27 14:43:44 +00:00
Migration 070 — добавляет houses.house_fias_id + dadata_qc_geo / qc_house /
enriched_at для tracking. Скрипт scripts/backfill_houses_dadata.py
проходит по ~3.8k rows с NULL coords + ~8.6k с NULL cadnum daily 100/day
(DaData demo quota → ~124 дня walltime для полного прохода).

Per-row SAVEPOINT, resume-safe (фильтр dadata_enriched_at IS NULL),
qc_geo IN (0,1) → overwrite coord-payload, иначе только tracking.
--priority coords|cadnum|both, --dry-run, --limit N (default 100).

20 тестов покрывают happy path qc_geo=0, qc_geo=3 city-level skip,
DaData None response, per-row exception isolation, --dry-run, --limit,
--priority filter, и missing-creds fail-fast.
Light1YT merged commit 08feb60a7f into main 2026-05-27 14:43:52 +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#606
No description provided.