-- 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;