feat(sf-b5): extend /parcels/{cad}/analyze with 15 fields (EGRN/encumbrance/red_lines/risks/district prices) #332

Merged
lekss361 merged 1 commit from feat/sf-b5-analyze-extend into main 2026-05-17 21:11:19 +00:00
Owner

Summary

SF Wave 1 / Group A / sub-task B5 из SiteFinder Backend Migration Plan May17. Extend POST /parcels/{cad_num}/analyze response с 15 новыми полями.

Files

  • backend/app/api/v1/parcels.py — extend response (competitors block untouched — уже работает после OBJ-3 + PR #328)

15 new fields

EGRN block (9):

  • egrn.cadastral_value_rub, cadastral_value_per_m2, land_category, permitted_use_text, last_egrn_update_date, area_m2, ownership_type, right_type, status, address

Encumbrance block (3):

  • encumbrance.has_zouit, zouit_types, zouit_count (из cad_zouit)

Red lines (2):

  • red_lines.intersects, count (через ST_Intersects на cad_red_lines)

Metro placeholder (1):

  • metro.nearest_top3 = null (заполнится после merge metro scraper)

District price ranges (4, из objective_lots по району):

  • district_price_per_m2_min/max/median/sample_size

Risks (3):

  • risks.flood_zone (cad_risk_zones), noise_score (osm_noise_sources_ekb), geology_risk_label (proxy)

Compatibility

  • competitors блок не trog'нут — OBJ-3 PR #324 + PR #328 продолжают работать
  • market_pulse, market_avg_price_per_m2 остаются (top-level keys)
  • Только additive — никаких breaking changes

Test plan

  • POST /api/v1/parcels/66:41:0204016:10/analyze → 200 + 15 новых полей
  • egrn.cadastral_value_rub populated (или null если нет в cad_parcels)
  • encumbrance.has_zouit boolean
  • district_price_per_m2_median ∈ [110k, 220k] для ЕКБ

Part of plan vault code/patterns/SiteFinder_Backend_Migration_Plan_May17.md §B5.

## Summary SF Wave 1 / Group A / sub-task **B5** из SiteFinder Backend Migration Plan May17. Extend POST `/parcels/{cad_num}/analyze` response с 15 новыми полями. ## Files - `backend/app/api/v1/parcels.py` — extend response (competitors block untouched — уже работает после OBJ-3 + PR #328) ## 15 new fields **EGRN block** (9): - `egrn.cadastral_value_rub`, `cadastral_value_per_m2`, `land_category`, `permitted_use_text`, `last_egrn_update_date`, `area_m2`, `ownership_type`, `right_type`, `status`, `address` **Encumbrance block** (3): - `encumbrance.has_zouit`, `zouit_types`, `zouit_count` (из `cad_zouit`) **Red lines** (2): - `red_lines.intersects`, `count` (через ST_Intersects на `cad_red_lines`) **Metro placeholder** (1): - `metro.nearest_top3` = null (заполнится после merge metro scraper) **District price ranges** (4, из objective_lots по району): - `district_price_per_m2_min/max/median/sample_size` **Risks** (3): - `risks.flood_zone` (`cad_risk_zones`), `noise_score` (`osm_noise_sources_ekb`), `geology_risk_label` (proxy) ## Compatibility - `competitors` блок не trog'нут — OBJ-3 PR #324 + PR #328 продолжают работать - `market_pulse`, `market_avg_price_per_m2` остаются (top-level keys) - Только additive — никаких breaking changes ## Test plan - [ ] `POST /api/v1/parcels/66:41:0204016:10/analyze` → 200 + 15 новых полей - [ ] `egrn.cadastral_value_rub` populated (или null если нет в `cad_parcels`) - [ ] `encumbrance.has_zouit` boolean - [ ] `district_price_per_m2_median` ∈ [110k, 220k] для ЕКБ Part of plan vault `code/patterns/SiteFinder_Backend_Migration_Plan_May17.md` §B5.
lekss361 added 1 commit 2026-05-17 21:04:04 +00:00
SF-B5: добавить 15 новых полей в POST /parcels/{cad_num}/analyze:
- egrn: cadastral_value_rub/per_m2, land_category, permitted_use_text,
  last_egrn_update_date, area_m2, ownership_type, right_type, status, address
- encumbrance: has_zouit, zouit_types, zouit_count (из cad_zouit)
- red_lines: intersects, count (из cad_red_lines via ST_Intersects)
- metro: nearest_top3 = null placeholder (заполнится после merge metro scraper)
- district_price_per_m2_min/max/median/sample_size (из objective_lots по району)
- risks: flood_zone (cad_risk_zones), noise_score, geology_risk_label (proxy)

Не трогает competitors block (OBJ-3 + PR #328 уже работают).
Author
Owner

Deep Code Review — verdict: APPROVE

Files: 1 (P1: backend/app/api/v1/parcels.py) · +221/-0 · purely additive

Field-by-field

Block Source Verified
egrn (10 fields) cad_parcels PK lookup All cols exist, NULL-safe, div-by-zero guarded (_area_m2 > 0), dates ISO-formatted
encumbrance (3) cad_zouit ST_Intersects GIST-indexed, set-dedup, safe default
red_lines (2) cad_red_lines ST_Intersects GIST-indexed, COUNT->int
metro (1) static placeholder OK pending 22h scraper merge
district_price_* (4) objective_lots district median PERCENTILE_CONT(0.5) (NOT AVG-over-zero — explicitly avoids Bug #21 shape), sample_size>0 gate, outlier filter 30k-600k, round-w-None guards
risks (3) cad_risk_zones + hydrology proxy flood composite via OR, 65 dB heuristic for geology label documented

Compliance

  • psycopg v3: CAST(:c AS text) and CAST(:dn AS text) — no ::type trap (grep :[a-z_]+::[a-z] clean)
  • No import psycopg2, no print(), no f-string SQL, no hardcoded creds
  • Ruff line ≤100: OK
  • All referenced vars in scope: district_row (L1197), noise_db_max/noise_score (L1525/1546), hydrology (L1590), geom_wkt available

Backward compat / no regressions

  • obj_pricing CTE (L1351) — intact
  • market_pulse aggregates from OBJ-3 (L2146-2192) — intact
  • competitors block (L41 import, L2223 output) — intact
  • All new keys at top-level dict are additive — no removed/renamed fields

Performance

All spatial queries use existing GIST indexes (cad_zouit_geom_gist, cad_red_lines_geom_gist, cad_risk_zones_geom_gist). objective_lots WHERE uses objective_lots_district_class_idx. cad_parcels lookup hits PK. No N+1.

Minor nit (non-blocking)

Two separate ST_Intersects scans (red_lines + risk_zones) — could be one CTE for fewer planner calls, but separate try/except gives better partial degradation. Keep as-is.

Merging.

## Deep Code Review — verdict: APPROVE **Files**: 1 (P1: backend/app/api/v1/parcels.py) · +221/-0 · purely additive ### Field-by-field | Block | Source | Verified | |---|---|---| | `egrn` (10 fields) | `cad_parcels` PK lookup | All cols exist, NULL-safe, div-by-zero guarded (`_area_m2 > 0`), dates ISO-formatted | | `encumbrance` (3) | `cad_zouit` ST_Intersects | GIST-indexed, set-dedup, safe default | | `red_lines` (2) | `cad_red_lines` ST_Intersects | GIST-indexed, COUNT->int | | `metro` (1) | static placeholder | OK pending 22h scraper merge | | `district_price_*` (4) | `objective_lots` district median | `PERCENTILE_CONT(0.5)` (NOT AVG-over-zero — explicitly avoids Bug #21 shape), sample_size>0 gate, outlier filter 30k-600k, round-w-None guards | | `risks` (3) | `cad_risk_zones` + hydrology proxy | flood composite via OR, 65 dB heuristic for geology label documented | ### Compliance - psycopg v3: `CAST(:c AS text)` and `CAST(:dn AS text)` — no `::type` trap (grep `:[a-z_]+::[a-z]` clean) - No `import psycopg2`, no `print()`, no f-string SQL, no hardcoded creds - Ruff line ≤100: OK - All referenced vars in scope: `district_row` (L1197), `noise_db_max`/`noise_score` (L1525/1546), `hydrology` (L1590), `geom_wkt` available ### Backward compat / no regressions - `obj_pricing` CTE (L1351) — intact - `market_pulse` aggregates from OBJ-3 (L2146-2192) — intact - `competitors` block (L41 import, L2223 output) — intact - All new keys at top-level dict are additive — no removed/renamed fields ### Performance All spatial queries use existing GIST indexes (`cad_zouit_geom_gist`, `cad_red_lines_geom_gist`, `cad_risk_zones_geom_gist`). `objective_lots` WHERE uses `objective_lots_district_class_idx`. `cad_parcels` lookup hits PK. No N+1. ### Minor nit (non-blocking) Two separate ST_Intersects scans (red_lines + risk_zones) — could be one CTE for fewer planner calls, but separate try/except gives better partial degradation. Keep as-is. Merging.
lekss361 merged commit 493238447a into main 2026-05-17 21:11:19 +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#332
No description provided.