"""Классификация «сетевых обременений» из ЗОУИТ (#1070). Pure-function классификатор: по тексту ``type_zone`` ЗОУИТ (cad_zouit / НСПД-дамп) определяет, является ли зона *охранной зоной инженерной сети* — и если да, к какому виду сети относится (тепло/электро/газ/водоснабжение/связь/общая инженерная). Контекст (#1070 ООО «Брик»): теплосеть 66:41:0000000:90310 проходит через пятно застройки → реальное обременение. Эпик #1067 (слой ИРД для analyze) НЕ содержал явного среза «сетевые обременения». ЗОУИТ-данные cad_zouit уже текут в analyze (``quarter_dump_lookup._get_cad_zouit_overlaps``, #232/#243) и классифицируются в gate_verdict, но как ОБЩИЕ ЗОУИТ-overlaps без выделенного «сетевого» среза. Этот модуль выделяет сетевые охранные зоны в отдельную типизированную категорию, чтобы analyze/фронт могли показать «сетевое обременение (теплосеть)» как самостоятельный флаг. VERIFY против PROD (cad_zouit, 2026-06-14): сетевые охранные зоны РЕАЛЬНО присутствуют — ~1930 строк из 3485: • «Охранная зона инженерных коммуникаций» — 1281 • «Охранная зона тепловых сетей» — 548 • «Охранная зона объектов электроэнергетики …» — 69 • «Охранная зона трубопроводов (газопроводов …)» — 23 • «Охранная зона линий и сооружений связи» — 9 Зачем pure-function (без БД): тестируемо без фикстур, переиспользуемо и в ``_get_cad_zouit_overlaps`` (теггинг overlap-строк), и в ``gate_verdict`` (выделенный warning-код), без дублирования keyword-таблиц. """ from __future__ import annotations from typing import Literal # Виды сетевого обременения. 'general' — «инженерные коммуникации» без указания вида сети. NetworkKind = Literal[ "thermal", # теплоснабжение "electricity", # электросети / объекты электроэнергетики "gas", # газопроводы (входит в «трубопроводы …») "water", # водоснабжение / водоотведение / канализация "communication", # линии и сооружения связи "pipeline", # трубопроводы (нефте-/нефтепродукто-/аммиако- — не газ, но сетевое) "general", # «охранная зона инженерных коммуникаций» (вид сети не уточнён) ] # Человекочитаемые подписи (RU) для UI/детали обременения. NETWORK_KIND_LABELS: dict[str, str] = { "thermal": "теплоснабжение", "electricity": "электроснабжение", "gas": "газоснабжение", "water": "водоснабжение/водоотведение", "communication": "связь", "pipeline": "трубопровод", "general": "инженерные коммуникации", } # Упорядоченные правила «подстрока type_zone → вид сети». Порядок ВАЖЕН: более # специфичные виды проверяются раньше «general» (инженерные коммуникации). # Все подстроки lowercase; матч по `substring in type_zone.lower()`. # Сверено с distinct type_zone из cad_zouit PROD (2026-06-14). _NETWORK_RULES: tuple[tuple[NetworkKind, tuple[str, ...]], ...] = ( ("thermal", ("тепл",)), # «… тепловых сетей» ("electricity", ("электроэнерг", "электросет", "лэп", "линий электропередач")), ("gas", ("газопров", "газоснаб")), ("water", ("водопров", "водоснаб", "водоотвед", "канализ")), ("communication", ("линий и сооружений связи", "сооружений связи", "линий связи")), # «Охранная зона трубопроводов (газопроводов, нефтепроводов …)» — если газ не # сматчился выше, относим к pipeline (нефте-/нефтепродукто-/аммиако-проводы). ("pipeline", ("трубопров", "нефтепров", "нефтепродуктопров", "аммиакопров")), # Самое общее — «охранная зона инженерных коммуникаций» (вид сети не уточнён). ("general", ("инженерных коммуникаций", "инженерной инфраструктур", "инженерных сет")), ) # Зона должна быть именно ОХРАННОЙ (обременение), а не, например, «зона публичного # сервитута» без сетевой привязки. Требуем маркер охранной/защитной зоны ИЛИ явный # сетевой вид в тексте — иначе «инженерные коммуникации» в произвольной зоне дало бы # ложные срабатывания. На практике cad_zouit использует «Охранная зона …». _PROTECTION_MARKERS: tuple[str, ...] = ("охранная зона", "охранн") def classify_network_zone(type_zone: str | None) -> NetworkKind | None: """Определить вид сетевого обременения по тексту ``type_zone`` ЗОУИТ. Args: type_zone: текст типа зоны (cad_zouit.type_zone / НСПД-дамп). None/пусто → None. Returns: ``NetworkKind`` если зона — охранная зона инженерной сети; иначе ``None`` (зона не сетевая: СЗЗ, ЗРЗ, сервитут без сети, природные ЗОУИТ и т.п.). """ if not type_zone: return None tz = type_zone.lower() is_protection = any(m in tz for m in _PROTECTION_MARKERS) # ЗСО (зона санитарной охраны ИСТОЧНИКА водоснабжения) — это зона вокруг водозабора, # НЕ охранная зона водопроводной СЕТИ → не сетевое обременение. Иначе water-needles # («водоснаб») ложно отнесли бы ЗСО к сети и флипнули verdict на blocker. is_sanitary_source = "санитарн" in tz for kind, needles in _NETWORK_RULES: if any(n in tz for n in needles): # «general» (инженерные коммуникации) принимаем только при маркере охранной # зоны — иначе слишком широкая ловля. Специфичные виды (тепло/газ/…) сами по # себе достаточно сетевые, но в cad_zouit они тоже всегда «Охранная зона …». if kind == "general" and not is_protection: continue # water-зона санитарной охраны источника — не сеть, пропускаем. if kind == "water" and is_sanitary_source: continue return kind return None def network_kind_label(kind: NetworkKind | None) -> str | None: """RU-подпись вида сети для UI/детали (None → None).""" if kind is None: return None return NETWORK_KIND_LABELS.get(kind, kind) def is_network_obremenenie(type_zone: str | None) -> bool: """True если ЗОУИТ — сетевое обременение (любой вид сети).""" return classify_network_zone(type_zone) is not None __all__ = [ "NETWORK_KIND_LABELS", "NetworkKind", "classify_network_zone", "is_network_obremenenie", "network_kind_label", ]