gendesign/data/sql/133_zone_regulation_cache.sql
lekss361 e5b12d579a
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
feat(sf): zone_regulation_cache — ПЗЗ-регламент по zone_index + числовой экстракт (#1099)
Миграция 133_zone_regulation_cache.sql + zone_regulation.py: кэш ПЗЗ-регламента по (city, zone_index) — 3 списка ВРИ + сырой текст предельных параметров + regex-экстракт числовых (max_far/застройка/этажность/площадь). get-or-fetch через urbanCard. Резолв один раз на зону (~100/город). Self-contained, wiring в D9b. 10 тестов на реальных geoportal-строках.

Refs #1067.
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-06 19:31:28 +00:00

93 lines
8 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 133_zone_regulation_cache.sql
-- #1067 (Site Finder v2 / ИРД-content «GG-форсайт», C8b) — кэш ПЗЗ-регламента по индексу зоны.
--
-- Регламент ПЗЗ идентичен для ВСЕХ участков одной территориальной зоны (Ц-1, Ж-2, ...), а зон
-- на город ~100. Поэтому резолвить ВРИ+предельные параметры через ЕКБ-геопортал urbanCard на
-- каждый участок — расточительно (reverse-eng API без SLA, см. ekb_geoportal_client.py). Кэшируем
-- ОДИН раз на (city, zone_index): 3 списка ВРИ (540н) + сырой текст предельных параметров +
-- числовой экстракт (regex) ключевых параметров застройки для capacity-оценки.
--
-- Что отдаёт urbanCard (live-verified 2026-06-06, vault research/EKB_Geoportal_PZZ_Content_API_Jun06):
-- pzzList[0]: urbanTerZone (index+имя) · main/condition/addition TypesPermittedUse (ВРИ) ·
-- limitTypesPermittedUse[].typePermittedUsePars[].otherDescription — предельные параметры
-- СВОБОДНЫМ ТЕКСТОМ (min площадь ЗУ / max % застройки / КСИ / подземная застройка / этажность),
-- часто «не подлежат установлению». Парсятся сервисом zone_regulation.py (regex) → params_numeric.
--
-- Зерно (grain): одна строка = один индекс зоны в городе. Дедуп-ключ — (city, zone_index).
-- re-fetch UPSERT-ит (latest wins): регламент меняется только при правке ПЗЗ (редко) →
-- fetched_at = freshness, refresh-воркер дёргает по возрасту.
--
-- Связь с ird_overlays (м.132): ird_overlays.zone_index (когда резолвлен) JOIN-ится сюда за
-- полным регламентом зоны участка. content в ird_overlays НЕ дублируется — single source тут.
--
-- Deploy: auto-applied deploy.yml через _schema_migrations (ровно один раз по NN). Idempotent:
-- CREATE TABLE/INDEX IF NOT EXISTS + guarded DO для UNIQUE (constraint нельзя добавить дважды,
-- прецедент м.40/110/125/132). НИКАКИХ строк не вставляется (данные — из сервиса C8b).
BEGIN;
-- ── 1. Таблица zone_regulation_cache ───────────────────────────────────────────
CREATE TABLE IF NOT EXISTS zone_regulation_cache (
id BIGSERIAL PRIMARY KEY,
city TEXT NOT NULL DEFAULT 'ekb', -- мульти-город forward-compat; пока только ЕКБ
zone_index TEXT NOT NULL, -- индекс ПЗЗ-зоны ('Ц-1', 'Ж-2', 'ЦС-1')
zone_full_name TEXT, -- 'Ц-1 Общественно-деловая зона городского центра'
-- ── ВРИ (списки строк «код 540н + текст», JSONB-массивы) ──
main_vri JSONB NOT NULL DEFAULT '[]'::jsonb, -- основные ВРИ
conditional_vri JSONB NOT NULL DEFAULT '[]'::jsonb, -- условно-разрешённые ВРИ
auxiliary_vri JSONB NOT NULL DEFAULT '[]'::jsonb, -- вспомогательные ВРИ
-- ── предельные параметры ──
limit_params_raw JSONB NOT NULL DEFAULT '[]'::jsonb, -- сырые otherDescription-строки (forward-compat/LLM)
params_numeric JSONB NOT NULL DEFAULT '{}'::jsonb, -- структурный экстракт: {param: [values...]} (regex, C8b)
-- ── headline-скаляры для capacity (most-permissive из найденных; NULL = «не подлежат установлению»/нет) ──
max_far NUMERIC, -- коэф. строит. использования ЗУ (КСИ), max найденный
max_building_pct NUMERIC, -- макс. процент застройки, max найденный (%)
underground_pct NUMERIC, -- процент застройки подземного пространства, max (%)
min_parcel_area_m2 NUMERIC, -- мин. площадь ЗУ, наименьший из найденных минимумов (кв. м)
max_floors INTEGER, -- предельная этажность, max найденный
max_height_m NUMERIC, -- предельная высота, max найденный (м)
source TEXT NOT NULL DEFAULT 'ekb_geoportal', -- источник регламента
fetched_at timestamptz NOT NULL DEFAULT now(), -- когда резолвили (freshness для refresh)
-- city/zone_index — осмысленные только непустыми; ловим мусор громко на write.
CONSTRAINT zone_regulation_cache_zone_index_chk CHECK (length(zone_index) > 0),
CONSTRAINT zone_regulation_cache_city_chk CHECK (length(city) > 0)
);
COMMENT ON TABLE zone_regulation_cache IS
'Кэш ПЗЗ-градрегламента по индексу зоны (#1067 C8b, GG-форсайт). 1 строка = 1 (city, '
'zone_index). 3 списка ВРИ (540н) + сырой текст предельных параметров + числовой экстракт '
'(params_numeric/headline-скаляры, regex). Резолв из ЕКБ-геопортала urbanCard (zone_regulation.py). '
'UPSERT по uq_zone_regulation_cache (city, zone_index); latest wins. ird_overlays.zone_index JOIN-ится сюда.';
COMMENT ON COLUMN zone_regulation_cache.params_numeric IS
'Числовой экстракт предельных параметров (regex из limit_params_raw): {max_far:[..], '
'max_building_pct:[..], underground_pct:[..], min_parcel_area_m2:[..], max_floors:[..], '
'max_height_m:[..]}. Списки т.к. параметр повторяется per-ВРИ. headline-скаляры — агрегат.';
COMMENT ON COLUMN zone_regulation_cache.max_far IS
'Коэф. строит. использования ЗУ (КСИ) — max найденный (capacity best-case). NULL = не установлен';
COMMENT ON COLUMN zone_regulation_cache.min_parcel_area_m2 IS
'Мин. площадь ЗУ — НАИМЕНЬШИЙ из найденных минимумов (самый разрешительный). NULL = не установлен';
-- ── 2. Дедуп/UPSERT-ключ (city, zone_index) ────────────────────────────────────
-- Цель ON CONFLICT для сервиса C8b: re-fetch обновляет регламент той же зоны, не плодя дубли.
-- Guard по pg_constraint → idempotent re-apply (прецедент м.125/132).
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'uq_zone_regulation_cache'
) THEN
ALTER TABLE zone_regulation_cache
ADD CONSTRAINT uq_zone_regulation_cache
UNIQUE (city, zone_index);
END IF;
END
$$;
-- ── 3. Индексы ─────────────────────────────────────────────────────────────────
-- Главный read-path: lookup регламента по индексу зоны (UNIQUE уже покрывает (city, zone_index),
-- но частый запрос — по zone_index без city). Очередь refresh — по возрасту (fetched_at).
CREATE INDEX IF NOT EXISTS idx_zone_regulation_cache_zone_index
ON zone_regulation_cache (zone_index);
CREATE INDEX IF NOT EXISTS idx_zone_regulation_cache_fetched_at
ON zone_regulation_cache (fetched_at);
COMMIT;