feat(sf-b5): extend /parcels/{cad}/analyze with 15 fields (EGRN/encumbrance/red_lines/risks/district prices) (#332)
This commit is contained in:
parent
4040cba4c0
commit
493238447a
1 changed files with 221 additions and 0 deletions
|
|
@ -1702,6 +1702,217 @@ def analyze_parcel(
|
|||
except Exception as e:
|
||||
logger.warning("parcel_meta query failed for %s: %s", cad_num, e)
|
||||
|
||||
# B5-1) EGRN block — расширенные данные из cad_parcels (SF-B5)
|
||||
egrn_block: dict[str, Any] = {}
|
||||
try:
|
||||
egrn_row = (
|
||||
db.execute(
|
||||
text("""
|
||||
SELECT cost_value AS cadastral_value_rub,
|
||||
cost_index AS cost_index_per_m2,
|
||||
land_record_category_type AS land_category,
|
||||
permitted_use_established_by_document AS permitted_use_text,
|
||||
cost_registration_date AS last_egrn_update_date,
|
||||
land_record_area AS area_m2,
|
||||
ownership_type,
|
||||
right_type,
|
||||
status,
|
||||
readable_address,
|
||||
registration_date
|
||||
FROM cad_parcels
|
||||
WHERE cad_num = CAST(:c AS text)
|
||||
LIMIT 1
|
||||
"""),
|
||||
{"c": cad_num},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
if egrn_row:
|
||||
_cad_val = (
|
||||
float(egrn_row["cadastral_value_rub"])
|
||||
if egrn_row["cadastral_value_rub"] is not None
|
||||
else None
|
||||
)
|
||||
_area_m2 = float(egrn_row["area_m2"]) if egrn_row["area_m2"] is not None else None
|
||||
_idx = egrn_row["cost_index_per_m2"]
|
||||
_cad_per_m2: float | None = None
|
||||
if _idx is not None:
|
||||
_cad_per_m2 = float(_idx)
|
||||
elif _cad_val is not None and _area_m2 and _area_m2 > 0:
|
||||
_cad_per_m2 = round(_cad_val / _area_m2, 2)
|
||||
egrn_block = {
|
||||
"cadastral_value_rub": _cad_val,
|
||||
"cadastral_value_per_m2": _cad_per_m2,
|
||||
"land_category": egrn_row["land_category"],
|
||||
"permitted_use_text": egrn_row["permitted_use_text"],
|
||||
"last_egrn_update_date": (
|
||||
egrn_row["last_egrn_update_date"].isoformat()
|
||||
if egrn_row["last_egrn_update_date"] is not None
|
||||
else None
|
||||
),
|
||||
"area_m2": _area_m2,
|
||||
"ownership_type": egrn_row["ownership_type"],
|
||||
"right_type": egrn_row["right_type"],
|
||||
"parcel_status": egrn_row["status"],
|
||||
"address": egrn_row["readable_address"],
|
||||
"registration_date": (
|
||||
egrn_row["registration_date"].isoformat()
|
||||
if egrn_row["registration_date"] is not None
|
||||
else None
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("egrn_block query failed for %s: %s", cad_num, e)
|
||||
|
||||
# B5-2) Encumbrance block — ЗОУИТ из cad_zouit (SF-B5)
|
||||
encumbrance_block: dict[str, Any] = {
|
||||
"has_zouit": False,
|
||||
"zouit_types": [],
|
||||
"zouit_count": 0,
|
||||
}
|
||||
try:
|
||||
zouit_rows = (
|
||||
db.execute(
|
||||
text("""
|
||||
SELECT type_zone, name_by_doc
|
||||
FROM cad_zouit
|
||||
WHERE ST_Intersects(geom, ST_GeomFromText(:wkt, 4326))
|
||||
ORDER BY id
|
||||
"""),
|
||||
{"wkt": geom_wkt},
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
)
|
||||
if zouit_rows:
|
||||
_zouit_types = list({r["type_zone"] for r in zouit_rows if r["type_zone"]})
|
||||
encumbrance_block = {
|
||||
"has_zouit": True,
|
||||
"zouit_types": _zouit_types,
|
||||
"zouit_count": len(zouit_rows),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("encumbrance_block query failed for %s: %s", cad_num, e)
|
||||
|
||||
# B5-3) Red lines block — пересечение с cad_red_lines (SF-B5)
|
||||
red_lines_block: dict[str, Any] = {"intersects": False, "count": 0}
|
||||
try:
|
||||
rl_row = (
|
||||
db.execute(
|
||||
text("""
|
||||
SELECT COUNT(*) AS cnt
|
||||
FROM cad_red_lines
|
||||
WHERE ST_Intersects(
|
||||
geom::geometry,
|
||||
ST_GeomFromText(:wkt, 4326)
|
||||
)
|
||||
"""),
|
||||
{"wkt": geom_wkt},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
if rl_row:
|
||||
_rl_cnt = int(rl_row["cnt"])
|
||||
red_lines_block = {
|
||||
"intersects": _rl_cnt > 0,
|
||||
"count": _rl_cnt,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("red_lines_block query failed for %s: %s", cad_num, e)
|
||||
|
||||
# B5-4) Metro placeholder — заполнится после merge 22h metro scraper
|
||||
metro_block: dict[str, Any] = {"nearest_top3": None}
|
||||
|
||||
# B5-5) District price ranges из objective_lots (SF-B5)
|
||||
district_price_block: dict[str, Any] = {
|
||||
"district_price_per_m2_min": None,
|
||||
"district_price_per_m2_max": None,
|
||||
"district_price_per_m2_median": None,
|
||||
"district_price_sample_size": None,
|
||||
}
|
||||
if district_row and district_row["district_name"]:
|
||||
try:
|
||||
dp_row = (
|
||||
db.execute(
|
||||
text("""
|
||||
SELECT
|
||||
MIN(price_per_m2_rub) AS price_min,
|
||||
MAX(price_per_m2_rub) AS price_max,
|
||||
PERCENTILE_CONT(0.5) WITHIN GROUP (
|
||||
ORDER BY price_per_m2_rub
|
||||
) AS price_median,
|
||||
COUNT(*) AS sample_size
|
||||
FROM objective_lots
|
||||
WHERE district = CAST(:dn AS text)
|
||||
AND price_per_m2_rub IS NOT NULL
|
||||
AND price_per_m2_rub BETWEEN 30000 AND 600000
|
||||
"""),
|
||||
{"dn": district_row["district_name"]},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
if dp_row and dp_row["sample_size"] and int(dp_row["sample_size"]) > 0:
|
||||
district_price_block = {
|
||||
"district_price_per_m2_min": (
|
||||
round(float(dp_row["price_min"])) if dp_row["price_min"] else None
|
||||
),
|
||||
"district_price_per_m2_max": (
|
||||
round(float(dp_row["price_max"])) if dp_row["price_max"] else None
|
||||
),
|
||||
"district_price_per_m2_median": (
|
||||
round(float(dp_row["price_median"])) if dp_row["price_median"] else None
|
||||
),
|
||||
"district_price_sample_size": int(dp_row["sample_size"]),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("district_price_block query failed for %s: %s", cad_num, e)
|
||||
|
||||
# B5-6) Risk indicators — flood_zone из cad_risk_zones + noise_score + geology proxy (SF-B5)
|
||||
risks_block: dict[str, Any] = {
|
||||
"flood_zone": False,
|
||||
"noise_score": round(noise_score, 2),
|
||||
"geology_risk_label": None,
|
||||
}
|
||||
try:
|
||||
flood_row = (
|
||||
db.execute(
|
||||
text("""
|
||||
SELECT COUNT(*) AS cnt
|
||||
FROM cad_risk_zones
|
||||
WHERE ST_Intersects(
|
||||
geom::geometry,
|
||||
ST_GeomFromText(:wkt, 4326)
|
||||
)
|
||||
AND (risk_type ILIKE '%flood%' OR risk_type ILIKE '%подтоп%'
|
||||
OR risk_type ILIKE '%затоп%')
|
||||
"""),
|
||||
{"wkt": geom_wkt},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
_flood = bool(flood_row and int(flood_row["cnt"]) > 0)
|
||||
# Geology proxy через hydrology flood_risk_flag (уже посчитан выше)
|
||||
_geo_flood = hydrology.get("flood_risk_flag", False) if hydrology else False
|
||||
_has_flood = _flood or _geo_flood
|
||||
# geology_risk_label: high если flooding, medium если шум > 65дБ, иначе low
|
||||
if _has_flood:
|
||||
_geo_label: str | None = "high"
|
||||
elif noise_db_max >= 65.0:
|
||||
_geo_label = "medium"
|
||||
else:
|
||||
_geo_label = "low"
|
||||
risks_block = {
|
||||
"flood_zone": _has_flood,
|
||||
"noise_score": round(noise_score, 2),
|
||||
"geology_risk_label": _geo_label,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("risks_block query failed for %s: %s", cad_num, e)
|
||||
|
||||
# 10) Market trend — динамика цен ДДУ в радиусе 3 км за 6 vs предыдущие 6 месяцев
|
||||
market_trend: dict[str, Any] | None = None
|
||||
try:
|
||||
|
|
@ -2300,6 +2511,16 @@ def analyze_parcel(
|
|||
},
|
||||
# #254: custom POI scoring — user-defined points (via X-Session-Id header).
|
||||
"custom_poi_score_items": custom_poi_items,
|
||||
# SF-B5: EGRN + encumbrance + red_lines + metro + district prices + risks
|
||||
"egrn": egrn_block,
|
||||
"encumbrance": encumbrance_block,
|
||||
"red_lines": red_lines_block,
|
||||
"metro": metro_block,
|
||||
"district_price_per_m2_min": district_price_block["district_price_per_m2_min"],
|
||||
"district_price_per_m2_max": district_price_block["district_price_per_m2_max"],
|
||||
"district_price_per_m2_median": district_price_block["district_price_per_m2_median"],
|
||||
"district_price_sample_size": district_price_block["district_price_sample_size"],
|
||||
"risks": risks_block,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue