diff --git a/backend/app/api/v1/parcels.py b/backend/app/api/v1/parcels.py index b3250e1e..48f99e56 100644 --- a/backend/app/api/v1/parcels.py +++ b/backend/app/api/v1/parcels.py @@ -1214,19 +1214,19 @@ def analyze_parcel( g.geom AS geom_wkb, 'cad_quarter' AS source FROM cad_quarters_geom g - WHERE g.cad_number = :c + WHERE g.cad_number = :c AND g.geom IS NOT NULL UNION ALL SELECT ST_AsGeoJSON(b.geom) AS geom_geojson, b.geom AS geom_wkb, 'cad_building' AS source FROM cad_buildings b - WHERE b.cad_num = :c + WHERE b.cad_num = :c AND b.geom IS NOT NULL UNION ALL SELECT ST_AsGeoJSON(p.geom) AS geom_geojson, p.geom AS geom_wkb, 'cad_parcel' AS source FROM cad_parcels_geom p - WHERE p.cad_num = :c + WHERE p.cad_num = :c AND p.geom IS NOT NULL LIMIT 1 """), {"c": cad_num}, @@ -1235,6 +1235,10 @@ def analyze_parcel( .first() ) + # NB: geom IS NOT NULL во всех ветках — участок с meta-строкой, но NULL-geometry + # (964 таких в cad_parcels_geom) НЕ должен «найтись» с пустой геометрией (иначе + # ST_Centroid→NULL→float(None)→500). Без geom он попадает в #93 graceful fallback + # ниже (enqueue NSPD fetch → 202), как и участок, которого нет в БД вовсе. if not row: # #93 — graceful fallback: enqueue NSPD fetch, await inline до 15s # (см. _INLINE_FETCH_WAIT_S — снижено с 25 для threadpool safety). @@ -1260,15 +1264,15 @@ def analyze_parcel( g.geom AS geom_wkb, 'cad_quarter' AS source FROM cad_quarters_geom g - WHERE g.cad_number = :c + WHERE g.cad_number = :c AND g.geom IS NOT NULL UNION ALL SELECT ST_AsGeoJSON(b.geom), b.geom, 'cad_building' FROM cad_buildings b - WHERE b.cad_num = :c + WHERE b.cad_num = :c AND b.geom IS NOT NULL UNION ALL SELECT ST_AsGeoJSON(p.geom), p.geom, 'cad_parcel' FROM cad_parcels_geom p - WHERE p.cad_num = :c + WHERE p.cad_num = :c AND p.geom IS NOT NULL LIMIT 1 """), {"c": cad_num}, @@ -1299,11 +1303,14 @@ def analyze_parcel( text(""" SELECT ST_AsText(g.geom) AS wkt FROM ( - SELECT g.geom FROM cad_quarters_geom g WHERE g.cad_number = :c + SELECT g.geom FROM cad_quarters_geom g + WHERE g.cad_number = :c AND g.geom IS NOT NULL UNION ALL - SELECT b.geom FROM cad_buildings b WHERE b.cad_num = :c + SELECT b.geom FROM cad_buildings b + WHERE b.cad_num = :c AND b.geom IS NOT NULL UNION ALL - SELECT p.geom FROM cad_parcels_geom p WHERE p.cad_num = :c + SELECT p.geom FROM cad_parcels_geom p + WHERE p.cad_num = :c AND p.geom IS NOT NULL ) g LIMIT 1 """), @@ -1586,8 +1593,15 @@ def analyze_parcel( .mappings() .first() ) - centroid_lat: float = float(centroid_row["lat"]) if centroid_row else 56.838 - centroid_lon: float = float(centroid_row["lon"]) if centroid_row else 60.605 + # Defensive: centroid_row может существовать, но с NULL lat/lon (вырожденная/ + # невалидная геометрия → ST_Centroid NULL). Проверяем именно значения, а не только + # наличие строки — иначе float(None) → 500. Fallback = центр ЕКБ. + centroid_lat: float = ( + float(centroid_row["lat"]) if centroid_row and centroid_row["lat"] is not None else 56.838 + ) + centroid_lon: float = ( + float(centroid_row["lon"]) if centroid_row and centroid_row["lon"] is not None else 60.605 + ) # 6b) Distance to EKB center + center bonus dist_to_center_km = _haversine_km(centroid_lat, centroid_lon, EKB_CENTER_LAT, EKB_CENTER_LON)