-- 137_listings_addr_norm_trgm.sql
-- Trigram GIN expression-индекс на lower(translate(address,'ёЁ','ее')) —
-- ускоряет same-building anchor Tier A эстиматора (горячий, на КАЖДОЙ оценке).
--
-- WHY (perf-аудит /estimate, EXPLAIN-решение об индексе):
--   `_fetch_anchor_comps` Tier A (estimator.py, anchor-путь за флагом
--   estimate_same_building_anchor_enabled=True — дефолт ON, в проде не
--   переопределён) запускается на каждой оценке с распознаваемым номером дома.
--   Предикат:
--       lower(translate(address,'ёЁ','ее')) LIKE '%<street>%'   -- substring
--       lower(translate(address,'ёЁ','ее')) ~ '(^|[^0-9])<no>...'-- regex
--   Оба условия оборачивают address в функцию lower(translate(...)) →
--   non-sargable. Существующий listings_address_trgm_idx = gin(address
--   gin_trgm_ops) построен на СЫРОМ address, поэтому планировщик НЕ может его
--   применить к нормализованному выражению → Seq Scan по всем ~66.7k строкам.
--
--   Прод-EXPLAIN (2026-06-27, ЕКБ "Хохрякова 48", 18 строк-результат):
--     было  — Seq Scan on listings: Rows Removed by Filter 66714,
--             read=15329 buffers → 167 мс.
--     стало — Bitmap Index Scan on listings_addr_norm_trgm_idx
--             (LIKE %s% И ~regex оба как Index Cond) → 97 buffers → 1.77 мс.
--             (~94× быстрее, ~98% меньше прочитанных буферов). Замерено dry-run'ом
--             (BEGIN; CREATE INDEX; EXPLAIN ANALYZE; ROLLBACK) на реальном проде.
--   Эквивалентность результата проверена: count/sum/min/max address с индексом и
--   без — байт-в-байт (18 строк, sum(price_per_m2)=8 536 769). Индекс — только
--   путь доступа, выборка не меняется.
--
-- WHAT:
--   GIN trgm-индекс по выражению lower(translate(address,'ёЁ','ее')). Выражение
--   ТОЧНО совпадает с тем, как Tier A пишет предикат — без правок кода. gin_trgm_ops
--   обслуживает И LIKE '%...%', И regex (~) — оба условия Tier A покрыты.
--   ё→е нормализация в индексе симметрична нормализатору адресов (#677/#679).
--
-- SAFETY:
--   - НЕ деструктивно: только добавление индекса, данные не трогаются.
--   - IF NOT EXISTS → идемпотентно при повторном прогоне (no-op).
--   - Размер ~8 МБ (меньше существующего listings_address_trgm_idx=16 МБ —
--     нормализация схлопывает часть триграмм). Сборка GIN по ~66.7k строк дёшева
--     (<1с), кратковременный ACCESS EXCLUSIVE lock на время сборки. CONCURRENTLY
--     не используем: файл обёрнут в BEGIN/COMMIT (CREATE INDEX CONCURRENTLY в
--     транзакции нельзя), на таком объёме обычная сборка дёшева. NB: раннер
--     деплоя (deploy.yml) гонит файл через psql -v ON_ERROR_STOP=on БЕЗ
--     --single-transaction, т.е. транзакцию открывает сам файл — как 134/133/104
--     и house-rule (Schema_Migrations_vs_Alembic.md #4).
--   - listings_address_trgm_idx (gin(address gin_trgm_ops)) ОСТАВЛЯЕМ — он на
--     сыром address и нужен другим путям (ILIKE по необработанному адресу).
--
-- DEPENDENCIES:
--   - pg_trgm extension (присутствует на проде — verified 2026-06-27).
--   - listings.address text -- 011_listings_alter.sql.
--
-- Deploy order: after 136_backfill_listings_house_id_fk_source_identity.sql.

BEGIN;

CREATE INDEX IF NOT EXISTS listings_addr_norm_trgm_idx
    ON listings USING gin (lower(translate(address, 'ёЁ', 'ее')) gin_trgm_ops)
    WHERE address IS NOT NULL;

COMMIT;
