KILLER feature от Макса — точки подключения к инженерным сетям + охранные зоны как proxy для подбора инвест-участков. Phase 1 — публичные NSPD данные (cat 36328 + 37578). - ConnectionPointsResponse pydantic + 3 nested models - get_connection_points() + helpers (parcel WKT, structures by boundary, zouit overlaps) с ST_Distance(::geography) для метров - GET endpoint с Query(radius_m, ge=50, le=2000, default=500) - 7 tests Closes part of #115 (Phase 1 backend).
91 lines
2.3 KiB
Python
91 lines
2.3 KiB
Python
from typing import Any
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
# ── Connection points schemas (issue #115) ────────────────────────────────────
|
|
|
|
|
|
class EngineeringStructure(BaseModel):
|
|
name: str | None
|
|
type: str | None
|
|
cad_num: str | None
|
|
distance_to_boundary_m: float
|
|
geometry_geojson: dict[str, Any]
|
|
readable_address: str | None
|
|
raw_props: dict[str, Any]
|
|
source: str
|
|
|
|
|
|
class ZouitOverlap(BaseModel):
|
|
reg_numb_border: str | None
|
|
type_zone: str | None
|
|
subcategory: int | None
|
|
intersects_parcel: bool
|
|
geometry_geojson: dict[str, Any]
|
|
raw_props: dict[str, Any]
|
|
source: str
|
|
|
|
|
|
class ConnectionPointsSummary(BaseModel):
|
|
nearest_structure_distance_m: float | None
|
|
in_protection_zone: bool
|
|
protection_zones_intersecting: int
|
|
total_structures_in_radius: int
|
|
|
|
|
|
class ConnectionPointsResponse(BaseModel):
|
|
engineering_structures: list[EngineeringStructure]
|
|
zouit_engineering_overlaps: list[ZouitOverlap]
|
|
summary: ConnectionPointsSummary
|
|
dump_available: bool
|
|
dump_fetched_at: str | None
|
|
|
|
|
|
# ── Parcel search/detail schemas ──────────────────────────────────────────────
|
|
|
|
|
|
class ParcelFilter(BaseModel):
|
|
vri: list[str] | None = None
|
|
area_min_sqm: float | None = None
|
|
area_max_sqm: float | None = None
|
|
region: str | None = None
|
|
polygon_geojson: dict[str, Any] | None = None
|
|
text_search: str | None = None
|
|
|
|
|
|
class ScoringWeights(BaseModel):
|
|
distance_to_center: float = 0.4
|
|
poi_density: float = 0.3
|
|
neighborhood_price: float = 0.3
|
|
|
|
|
|
class ParcelSearchRequest(BaseModel):
|
|
filters: ParcelFilter = Field(default_factory=ParcelFilter)
|
|
weights: ScoringWeights = Field(default_factory=ScoringWeights)
|
|
limit: int = 50
|
|
|
|
|
|
class ScoreBreakdown(BaseModel):
|
|
total: float
|
|
distance_to_center: float
|
|
poi_density: float
|
|
neighborhood_price: float
|
|
|
|
|
|
class ParcelSummary(BaseModel):
|
|
id: str
|
|
cadastral_number: str
|
|
area_sqm: float
|
|
vri: str
|
|
address: str | None
|
|
score: ScoreBreakdown
|
|
|
|
|
|
class ParcelSearchResponse(BaseModel):
|
|
items: list[ParcelSummary]
|
|
total: int
|
|
|
|
|
|
class ParcelDetail(ParcelSummary):
|
|
geometry_geojson: dict[str, Any]
|
|
enrichment: dict[str, Any]
|