"""Гуманизация «сетевых охранных зон» ЗОУИТ для §3 ПТИЦА (#2119 C3). Позитив-разрез охранных зон инженерных сетей: рядом с участком физически ПРОХОДИТ сеть (охранная зона = коридор сети) → «теплотрасса в 24 м — врезка дешёвая». Pure / без БД: на вход ``type_zone`` + ``name_by_doc`` (детальные имена из cad_zouit), на выход — уточнённый ``net_kind`` + человекочитаемый ``human_label`` для финдиректора. Зачем отдельно от ``network_obremenenie.classify_network_zone``: - ``classify_network_zone`` смотрит ТОЛЬКО на ``type_zone`` (грубый вид: thermal / general / …). На проде (2026-07-03) большинство «Охранная зона инженерных коммуникаций» (``general``) — по ``name_by_doc`` РЕАЛЬНО теплосети/газопроводы («Трубопровод (тепловые сети) с кадастровым номером 66:41:…», «Газопровод низкого давления…»). Оставить их ``general`` = потерять решающий тепло-сигнал. - Поэтому здесь ``refine_net_kind`` дочищает ``general`` по ``name_by_doc``, а ``human_label`` вытаскивает кад.номер трубопровода / напряжение ВЛ / номер ТП regex-ами. Классификатор type_zone НЕ дублируем — переиспользуем как базу. """ from __future__ import annotations import re from app.services.site_finder.network_obremenenie import NetworkKind, classify_network_zone # Кад.номер РФ: 2:2:до7цифр:до-длинная последняя секция (напр. 66:41:0108079:76). # Последняя секция бывает с ведущими нулями («0036») — берём как есть, не нормализуем. _CAD_NUM_RE = re.compile(r"\b\d{2}:\d{2}:\d{6,7}:\d+\b") # «ВЛ 0,4 кВ» / «КЛ 10кВ» / «ВЛИ-0,4 кВ» — тип линии + напряжение. Запятая-десятичный # разделитель (0,4) и точка (0.4) оба валидны; пробел перед «кВ» опционален. _VL_KV_RE = re.compile( r"\b(ВЛИ|ВЛЗ|ВЛ|КЛ|КВЛ)\b[\s-]*?(\d+(?:[.,]\d+)?)\s*кВ", re.IGNORECASE, ) # Номер ТП/РП/КТП: «ТП 3164», «ТП 32505», «ТП 3736-3747», «КТП №589». Берём первый # «якорный» ТП (источник питания ввода). Номер — цифры, опционально с дефисом-диапазоном. _TP_RE = re.compile( r"\b(ТП|РП|КТП|КТПН|БКТП|РТП)\b\s*(?:п/?№|№|N)?\s*(\d+(?:-\d+)?)", re.IGNORECASE, ) # Маркеры «на самом деле тепло» в name_by_doc для general-зон (инженерные коммуникации). _THERMAL_NEEDLES: tuple[str, ...] = ( "теплоснабж", "тепловая сеть", "тепловые сети", "тепловых сетей", "теплосет", "теплотрасс", ) # Маркеры «на самом деле газ» в name_by_doc для general-зон. _GAS_NEEDLES: tuple[str, ...] = ( "газопров", "газораспредел", "газоснаб", "догазифик", ) # Маркеры электро (ВЛ/КЛ/электросет/подстанция) в name_by_doc для general-зон. # «трансформаторная подстанция» / «распределительный пункт» — объект электросети без # явного «ВЛ N кВ» (прод-кейс 66:41:0404019:333). ТП здесь = трансформаторная (электро), # не путать с тепловой ТК (тепловая камера) — та ловится _THERMAL_NEEDLES раньше. _ELECTRICITY_NEEDLES: tuple[str, ...] = ( "электросет", "электроснабж", "электросетевого", "кабельн", "трансформаторн", "распределительн", ) def refine_net_kind(type_zone: str | None, name_by_doc: str | None) -> NetworkKind | None: """Уточнить вид сети: база по ``type_zone``, дочистка ``general`` по ``name_by_doc``. Возвращает ``None``, если зона вообще не сетевая (как ``classify_network_zone``). Для ``general`` («инженерные коммуникации») пытается распознать реальный вид по детальному имени: тепло → thermal, газ → gas, электро → electricity. Если вид не распознан — остаётся ``general`` («вид не уточнён»). Специфичные type_zone (thermal / electricity / gas / …) НЕ переопределяем именем — для них type_zone уже точный, а name_by_doc лишь обогащает подпись. """ base = classify_network_zone(type_zone) if base != "general": return base name = (name_by_doc or "").lower() if any(n in name for n in _THERMAL_NEEDLES): return "thermal" if any(n in name for n in _GAS_NEEDLES): return "gas" # ВЛ/КЛ N кВ в имени → электро (regex поймает даже без слова «электросеть»). if any(n in name for n in _ELECTRICITY_NEEDLES) or _VL_KV_RE.search(name_by_doc or ""): return "electricity" return "general" def _fmt_voltage(raw: str) -> str: """«0,4» / «0.4» / «10» → нормализованная строка напряжения («0,4», «10»).""" return raw.replace(".", ",") def _is_last_mile_voltage(raw: str) -> bool: """Напряжение ≤1 кВ (0,4 кВ и т.п.) — last-mile ввод, не магистраль.""" try: return float(raw.replace(",", ".")) <= 1.0 except ValueError: return False def human_label(net_kind: NetworkKind | None, name_by_doc: str | None) -> str: """Человекочитаемая подпись сетевой зоны для §3 (финдиректор-facing). thermal → «Тепловая сеть (по охранной зоне)» + кад.номер трубопровода, если есть. electricity → «Ввод N кВ от ТП M (по охранной зоне)» (regex ВЛ/КЛ + ТП); напряжение ≤1 кВ подаём как «(ввод 0,4 кВ, last-mile)» — второстепенно. gas/pipeline → «Трубопровод (газо-/нефтепровод) по охранной зоне». general → «Инженерные коммуникации (вид не уточнён)». None → «Охранная зона» (fallback, не должно доходить для сетевых). """ name = name_by_doc or "" if net_kind == "thermal": cad = _CAD_NUM_RE.search(name) base = "Тепловая сеть (по охранной зоне)" if cad: return f"{base}, кад. № {cad.group(0)}" return base if net_kind == "electricity": vl = _VL_KV_RE.search(name) tp = _TP_RE.search(name) if vl: voltage = _fmt_voltage(vl.group(2)) last_mile = _is_last_mile_voltage(vl.group(2)) if tp: tp_num = tp.group(2) if last_mile: # ≤1 кВ — last-mile, подаём как второстепенный факт (уточнение в скобках). return f"Ввод от ТП {tp_num} (по охранной зоне, {voltage} кВ — last-mile)" return f"Ввод {voltage} кВ от ТП {tp_num} (по охранной зоне)" if last_mile: return f"Электроввод {voltage} кВ — last-mile (по охранной зоне)" return f"Ввод {voltage} кВ (по охранной зоне)" if tp: return f"Электросеть от ТП {tp.group(2)} (по охранной зоне)" return "Объект электросетевого хозяйства (по охранной зоне)" if net_kind in ("gas", "pipeline"): cad = _CAD_NUM_RE.search(name) base = "Трубопровод (газо-/нефтепровод) по охранной зоне" if cad: return f"{base}, кад. № {cad.group(0)}" return base if net_kind == "water": return "Сеть водоснабжения/водоотведения (по охранной зоне)" if net_kind == "communication": return "Линия связи (по охранной зоне)" if net_kind == "general": return "Инженерные коммуникации (вид не уточнён)" return "Охранная зона" __all__ = ["human_label", "refine_net_kind"]