fix(site-finder): address PR #91 auto-review minor feedback
Backend (parcels.py):
1. (medium) Aggregation loop _neighbors_summary теперь обёрнут в try/except
(ValueError, TypeError) с fallback к data_available:False + log warning.
Защищает от non-numeric cost_value/area придёт в строке (e.g. "N/A") —
ранее весь endpoint падал 500.
2. Magic numbers вынесены: _COST_PER_M2_MIN=1000, _COST_PER_M2_MAX=500_000.
3. _parse_floors docstring + inline note про malformed parts ("5а-7" filter,
multi-range "1-2-3" max acceptable degradation).
Frontend (NeighborsBlock.tsx):
5. Русский plural fix: pluralBuildings(n) helper — 1 здание, 2-4 здания,
5+/11-14 зданий. Раньше "3 зданий" — теперь "3 здания".
Не сделано (defer):
4. ST_Area для overlap query — практически 0-5 buildings в ЕКБ, GIST scan OK.
6. Discriminated union для NeighborsSummary — refactor а не bug.
7. Vault entry для P2 — добавится batch'ем после merge всех текущих PR.
Per auto-review on 60d53bb.
This commit is contained in:
parent
60d53bbf7d
commit
3706a79898
2 changed files with 45 additions and 17 deletions
|
|
@ -289,10 +289,19 @@ GEOTECH_BY_REGION: dict[int, dict[str, Any]] = {
|
|||
}
|
||||
|
||||
|
||||
# P2 (#46) cost-per-m² sanity filter — кадастровая стоимость иногда
|
||||
# содержит 0/None или экстремальные значения (миллиарды). Пороги выбраны
|
||||
# эмпирически для ЕКБ.
|
||||
_COST_PER_M2_MIN = 1000 # ₽/м² — ниже скорее всего ошибка ввода
|
||||
_COST_PER_M2_MAX = 500_000 # ₽/м² — выше скорее всего outlier
|
||||
|
||||
|
||||
def _parse_floors(raw: str | None) -> int | None:
|
||||
"""cad_buildings.floors хранится TEXT (могут быть диапазоны '1-2').
|
||||
"""cad_buildings.floors хранится TEXT (могут быть диапазоны '1-2', '5-7').
|
||||
|
||||
Возвращаем верхнюю границу (более консервативный сосед-высотка).
|
||||
NB: `isdigit()` намеренно фильтрует malformed parts типа "5а-7"; для
|
||||
multi-range "1-2-3" возвращается max(1,2,3)=3 (acceptable degradation).
|
||||
"""
|
||||
if not raw:
|
||||
return None
|
||||
|
|
@ -353,22 +362,30 @@ def _neighbors_summary(db: Session, geom_wkt: str, our_cad_num: str) -> dict[str
|
|||
logger.warning("neighbors query failed: %s", e)
|
||||
return {"data_available": False, "note": f"neighbors query failed: {e}"}
|
||||
|
||||
# Aggregate floors + cost
|
||||
floors_parsed: list[int] = []
|
||||
costs_per_m2: list[float] = []
|
||||
for r in neighbor_rows:
|
||||
f = _parse_floors(r.get("floors"))
|
||||
if f is not None and f > 0:
|
||||
floors_parsed.append(f)
|
||||
if r.get("cost_value") and r.get("area") and float(r["area"]) > 0:
|
||||
cost_per_m2 = float(r["cost_value"]) / float(r["area"])
|
||||
# sanity filter — кадастровая стоимость должна быть >0 + <500K ₽/м²
|
||||
if 1000 < cost_per_m2 < 500_000:
|
||||
costs_per_m2.append(cost_per_m2)
|
||||
# Aggregate floors + cost. Дефенсивный try/except: если cost_value/area
|
||||
# придёт как non-numeric (e.g. "N/A"), float() бросит ValueError и без
|
||||
# этого guard весь endpoint вернёт 500.
|
||||
try:
|
||||
floors_parsed: list[int] = []
|
||||
costs_per_m2: list[float] = []
|
||||
for r in neighbor_rows:
|
||||
f = _parse_floors(r.get("floors"))
|
||||
if f is not None and f > 0:
|
||||
floors_parsed.append(f)
|
||||
if r.get("cost_value") and r.get("area") and float(r["area"]) > 0:
|
||||
cost_per_m2 = float(r["cost_value"]) / float(r["area"])
|
||||
if _COST_PER_M2_MIN < cost_per_m2 < _COST_PER_M2_MAX:
|
||||
costs_per_m2.append(cost_per_m2)
|
||||
|
||||
avg_floors = round(sum(floors_parsed) / len(floors_parsed), 1) if floors_parsed else None
|
||||
max_floors = max(floors_parsed) if floors_parsed else None
|
||||
median_cost = round(sorted(costs_per_m2)[len(costs_per_m2) // 2]) if costs_per_m2 else None
|
||||
avg_floors = round(sum(floors_parsed) / len(floors_parsed), 1) if floors_parsed else None
|
||||
max_floors = max(floors_parsed) if floors_parsed else None
|
||||
median_cost = round(sorted(costs_per_m2)[len(costs_per_m2) // 2]) if costs_per_m2 else None
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning("neighbors aggregation failed: %s", e)
|
||||
return {
|
||||
"data_available": False,
|
||||
"note": f"neighbors aggregation failed: {e}",
|
||||
}
|
||||
|
||||
# Overlap check — что-то построено непосредственно на нашем участке.
|
||||
# Если хоть один building пересекается с площадью >50 м² — hard warn.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,17 @@ function fmtYearBuilt(y: number | null | undefined): string {
|
|||
return String(y);
|
||||
}
|
||||
|
||||
// Русский plural: 1 → "здание", 2-4 → "здания", 5+ → "зданий".
|
||||
// Также корректно для 11-14 (zданий), 21 (здание), 22 (здания) etc.
|
||||
function pluralBuildings(n: number): string {
|
||||
const mod10 = n % 10;
|
||||
const mod100 = n % 100;
|
||||
if (mod100 >= 11 && mod100 <= 14) return "зданий";
|
||||
if (mod10 === 1) return "здание";
|
||||
if (mod10 >= 2 && mod10 <= 4) return "здания";
|
||||
return "зданий";
|
||||
}
|
||||
|
||||
export function NeighborsBlock({ data }: Props) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
|
|
@ -94,7 +105,7 @@ export function NeighborsBlock({ data }: Props) {
|
|||
fontVariantNumeric: "tabular-nums",
|
||||
}}
|
||||
>
|
||||
{count} {count === 1 ? "здание" : "зданий"}
|
||||
{count} {pluralBuildings(count)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue