diff --git a/frontend/src/components/site-finder/EgrnPropertyTable.tsx b/frontend/src/components/site-finder/EgrnPropertyTable.tsx
new file mode 100644
index 00000000..1d42c21f
--- /dev/null
+++ b/frontend/src/components/site-finder/EgrnPropertyTable.tsx
@@ -0,0 +1,123 @@
+"use client";
+
+import type { EgrnData } from "@/types/site-finder";
+
+interface Props {
+ egrn: EgrnData;
+}
+
+function formatRub(v: number | null): string {
+ if (v == null) return "—";
+ if (v >= 1_000_000) return (v / 1_000_000).toFixed(1) + " млн ₽";
+ if (v >= 1_000) return (v / 1_000).toFixed(0) + " тыс ₽";
+ return v.toFixed(0) + " ₽";
+}
+
+function formatArea(v: number | null): string {
+ if (v == null) return "—";
+ return v.toFixed(0) + " м²";
+}
+
+function formatDate(v: string | null): string {
+ if (!v) return "—";
+ // ISO date → DD.MM.YYYY
+ const d = new Date(v);
+ if (isNaN(d.getTime())) return v;
+ return d.toLocaleDateString("ru-RU");
+}
+
+const ROWS: Array<{
+ label: string;
+ render: (e: EgrnData) => string;
+}> = [
+ { label: "Адрес", render: (e) => e.address ?? "—" },
+ { label: "Статус участка", render: (e) => e.parcel_status ?? "—" },
+ { label: "Категория земель", render: (e) => e.land_category ?? "—" },
+ { label: "ВРИ", render: (e) => e.permitted_use_text ?? "—" },
+ { label: "Форма собственности", render: (e) => e.ownership_type ?? "—" },
+ { label: "Вид права", render: (e) => e.right_type ?? "—" },
+ { label: "Площадь по ЕГРН", render: (e) => formatArea(e.area_m2) },
+ {
+ label: "Кадастровая стоимость",
+ render: (e) => formatRub(e.cadastral_value_rub),
+ },
+ {
+ label: "Кад. стоимость / м²",
+ render: (e) => formatRub(e.cadastral_value_per_m2),
+ },
+ {
+ label: "Дата обновления ЕГРН",
+ render: (e) => formatDate(e.last_egrn_update_date),
+ },
+];
+
+export function EgrnPropertyTable({ egrn }: Props) {
+ return (
+
+
+ ЕГРН — данные участка
+
+
+
+ {ROWS.map(({ label, render }, idx) => {
+ const value = render(egrn);
+ if (value === "—") return null;
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/frontend/src/components/site-finder/LandTab.tsx b/frontend/src/components/site-finder/LandTab.tsx
index ddc0fed7..da80f6a1 100644
--- a/frontend/src/components/site-finder/LandTab.tsx
+++ b/frontend/src/components/site-finder/LandTab.tsx
@@ -7,20 +7,55 @@ import { GeologyBlock } from "./GeologyBlock";
import { GeometrySuitabilityBlock } from "./GeometrySuitabilityBlock";
import { GeotechRiskBlock } from "./GeotechRiskBlock";
import { NeighborsBlock } from "./NeighborsBlock";
+import { NspdZouitOverlapsBlock } from "./NspdZouitOverlapsBlock";
+import { NspdEngineeringNearbyBlock } from "./NspdEngineeringNearbyBlock";
+import { EgrnPropertyTable } from "./EgrnPropertyTable";
interface Props {
data: ParcelAnalysis;
}
export function LandTab({ data }: Props) {
+ const hasZouit =
+ Array.isArray(data.nspd_zouit_overlaps) &&
+ data.nspd_zouit_overlaps.length > 0;
+ const hasEngineering =
+ !!data.utilities?.summary ||
+ (Array.isArray(data.nspd_engineering_nearby) &&
+ data.nspd_engineering_nearby.length > 0);
+ const hasEgrn = !!data.egrn;
const hasAny =
data.geotech_risk !== undefined ||
data.geology !== undefined ||
data.geometry_suitability !== undefined ||
- data.neighbors_summary !== undefined;
+ data.neighbors_summary !== undefined ||
+ hasZouit ||
+ hasEngineering ||
+ hasEgrn;
return (
+ {/* ЕГРН — данные участка из реестра */}
+ {hasEgrn &&
}
+
+ {/* ЗОУИТ — зоны с особыми условиями использования */}
+ {Array.isArray(data.nspd_zouit_overlaps) && (
+
+
+ Зоны с ограничениями (ЗОУИТ)
+
+
+
+ )}
+
+ {/* Инженерные сети / коммуникации */}
+ {hasEngineering && (
+
+ )}
+
{/* P2 (#46) — Соседи + overlap warning (hard warn если overlap) */}
{data.neighbors_summary && (
diff --git a/frontend/src/components/site-finder/NspdEngineeringNearbyBlock.tsx b/frontend/src/components/site-finder/NspdEngineeringNearbyBlock.tsx
new file mode 100644
index 00000000..d3ec0d14
--- /dev/null
+++ b/frontend/src/components/site-finder/NspdEngineeringNearbyBlock.tsx
@@ -0,0 +1,109 @@
+"use client";
+
+import type {
+ NspdEngineeringPoint,
+ UtilitiesSummary,
+} from "@/types/site-finder";
+
+interface Props {
+ utilities?: UtilitiesSummary | null;
+ points?: NspdEngineeringPoint[];
+}
+
+export function NspdEngineeringNearbyBlock({ utilities, points }: Props) {
+ const hasPoints = Array.isArray(points) && points.length > 0;
+ const hasSummary = !!utilities?.summary;
+
+ if (!hasSummary && !hasPoints) return null;
+
+ return (
+
+
+ Инженерные сети и коммуникации
+
+
+ {hasSummary && (
+
+ {utilities!.summary}
+
+ )}
+
+ {utilities?.power_line_охранная_зона_flag && (
+
+ ⚠
+ Охранная зона ЛЭП — строительство ограничено
+
+ )}
+
+ {hasPoints && (
+
+
+ Ближайшие точки подключения
+
+ {points!.map((pt, idx) => (
+
+ {pt.name ?? pt.type ?? "Коммуникация"}
+ {pt.distance_m !== null && (
+
+ {Math.round(pt.distance_m)} м
+
+ )}
+
+ ))}
+
+ )}
+
+ {utilities?.note && (
+
{utilities.note}
+ )}
+
+ );
+}
diff --git a/frontend/src/components/site-finder/NspdZouitOverlapsBlock.tsx b/frontend/src/components/site-finder/NspdZouitOverlapsBlock.tsx
new file mode 100644
index 00000000..9b298e7c
--- /dev/null
+++ b/frontend/src/components/site-finder/NspdZouitOverlapsBlock.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import type { NspdZouitOverlap } from "@/types/site-finder";
+
+interface Props {
+ overlaps: NspdZouitOverlap[];
+}
+
+export function NspdZouitOverlapsBlock({ overlaps }: Props) {
+ if (overlaps.length === 0) {
+ return (
+
+ Зоны с ограничениями (ЗОУИТ) не обнаружены
+
+ );
+ }
+
+ // Deduplicate by name to avoid showing the same zone twice (different sources)
+ const seen = new Set
();
+ const unique = overlaps.filter((o) => {
+ if (seen.has(o.name)) return false;
+ seen.add(o.name);
+ return true;
+ });
+
+ const isHardBlock = unique.some((o) =>
+ o.type_zone?.toLowerCase().includes("охранная"),
+ );
+
+ return (
+
+
+ ⚠
+
+ ЗОУИТ: {unique.length}{" "}
+ {unique.length === 1 ? "зона" : unique.length < 5 ? "зоны" : "зон"}
+
+
+
+
+ {unique.map((o, idx) => (
+
+
+ {o.type_zone ?? o.layer}
+
+
{o.name}
+
+ ))}
+
+
+
+ Источник: НСПД / Росреестр ЗОУИТ
+
+
+ );
+}
diff --git a/frontend/src/types/site-finder.ts b/frontend/src/types/site-finder.ts
index 7217fd6b..50129741 100644
--- a/frontend/src/types/site-finder.ts
+++ b/frontend/src/types/site-finder.ts
@@ -335,6 +335,46 @@ export interface ScoreGroupTotal {
contribution_pct: number;
}
+// B5 extended — utilities (коммуникации)
+export interface UtilitiesSummary {
+ summary: string;
+ power_line_охранная_зона_flag: boolean;
+ note: string;
+}
+
+// B5 extended — NSPD ЗОУИТ overlap
+export interface NspdZouitOverlap {
+ group_key: string;
+ layer: string;
+ subcategory: string | null;
+ name: string;
+ type_zone: string | null;
+ source: string;
+}
+
+// B5 extended — NSPD engineering nearby (точки подключения)
+export interface NspdEngineeringPoint {
+ name: string | null;
+ type: string | null;
+ distance_m: number | null;
+ lat: number | null;
+ lon: number | null;
+}
+
+// B5 extended — ЕГРН данные участка
+export interface EgrnData {
+ cadastral_value_rub: number | null;
+ cadastral_value_per_m2: number | null;
+ land_category: string | null;
+ permitted_use_text: string | null;
+ last_egrn_update_date: string | null;
+ area_m2: number | null;
+ ownership_type: string | null;
+ right_type: string | null;
+ parcel_status: string | null;
+ address: string | null;
+}
+
export interface ParcelAnalysis {
cad_num: string;
source: "cad_quarter" | "cad_building";
@@ -379,6 +419,11 @@ export interface ParcelAnalysis {
gate_verdict?: GateVerdict;
// D2 (#34) — velocity-score: темп продаж конкурентов
velocity?: Velocity | null;
+ // B5 extended — новые поля анализа
+ utilities?: UtilitiesSummary | null;
+ nspd_zouit_overlaps?: NspdZouitOverlap[];
+ nspd_engineering_nearby?: NspdEngineeringPoint[];
+ egrn?: EgrnData | null;
}
export type PoiCategory =