From 9cc8ab96f73537c4a6d9decef13e7c7312688025 Mon Sep 17 00:00:00 2001 From: TradeIn Deploy Date: Fri, 22 May 2026 10:54:42 +0500 Subject: [PATCH] =?UTF-8?q?feat(tradein):=20=D0=BA=D0=B0=D0=B4=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=20=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B9=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BA=20=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA?= =?UTF-8?q?=20house=5Fmetadata=20(#393)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В gendesign-БД есть cad_buildings — ~36к зданий ЕКБ с годом постройки и этажностью (Росреестр). Подключаем как ПРИОРИТЕТНЫЙ источник обогащения house_metadata, OSM Overpass остаётся fallback-ом. - data/sql/006_cad_buildings.sql — таблица cad_buildings в tradein-БД. - deploy/import-cadastre.sh — ETL: ЕКБ-срез cad_buildings из gendesign-postgres → tradein (по образцу import-rosreestr.sh). - house_metadata._cad_buildings_get — lookup по гео-близости (60 м), проверяется ПЕРВЫМ в get_house_metadata, до OSM. cad_buildings — кадастр ЗДАНИЙ, не квартир: поле ручного ввода кадастра (исходная идея #393) не делаем — гео-обогащение полезнее и работает автоматически для каждой оценки. Closes #393 --- .../backend/app/services/house_metadata.py | 43 ++++++++++ .../backend/data/sql/006_cad_buildings.sql | 34 ++++++++ tradein-mvp/deploy/import-cadastre.sh | 78 +++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 tradein-mvp/backend/data/sql/006_cad_buildings.sql create mode 100755 tradein-mvp/deploy/import-cadastre.sh diff --git a/tradein-mvp/backend/app/services/house_metadata.py b/tradein-mvp/backend/app/services/house_metadata.py index f1341dfc..e552693c 100644 --- a/tradein-mvp/backend/app/services/house_metadata.py +++ b/tradein-mvp/backend/app/services/house_metadata.py @@ -29,6 +29,7 @@ logger = logging.getLogger(__name__) OVERPASS_URL = "https://overpass-api.de/api/interpreter" _CACHE_RADIUS_M = 40 # дом в пределах 40 м от точки — считаем тем же _OVERPASS_RADIUS_M = 25 # ищем здание в 25 м от геокодированной точки +_CAD_RADIUS_M = 60 # радиус поиска здания в cad_buildings (#393) @dataclass(frozen=True, slots=True) @@ -137,6 +138,39 @@ def _cache_put(db: Session, meta: HouseMetadata, raw: dict) -> None: db.commit() +# ── Источник #393: кадастр зданий (cad_buildings, импорт из gendesign-БД) ──── +def _cad_buildings_get(db: Session, lat: float, lon: float) -> HouseMetadata | None: + """Ближайшее здание из cad_buildings (данные Росреестра) по координатам. + + Приоритетный источник house_metadata — год / этажность полнее, чем OSM + (~34к домов ЕКБ с годом постройки). Таблицу наполняет import-cadastre.sh. + """ + row = db.execute( + text( + """ + SELECT year_built, floors + FROM cad_buildings + WHERE geom IS NOT NULL + AND (year_built IS NOT NULL OR floors IS NOT NULL) + AND ST_DWithin(geom::geography, ST_MakePoint(:lon, :lat)::geography, :radius) + ORDER BY ST_Distance(geom::geography, ST_MakePoint(:lon, :lat)::geography) + LIMIT 1 + """ + ), + {"lat": lat, "lon": lon, "radius": _CAD_RADIUS_M}, + ).fetchone() + if row is None: + return None + return HouseMetadata( + lat=lat, lon=lon, + year_built=row.year_built, + total_floors=row.floors, + house_type=None, # cad_buildings.purpose не маппится в panel/brick + total_units=None, + source="cadastre", + ) + + # ── Provider: OSM Overpass ─────────────────────────────────────────────────── async def _overpass_lookup(lat: float, lon: float) -> tuple[HouseMetadata, dict] | None: """Запрос здания у точки через Overpass API. (meta, raw_payload) или None.""" @@ -181,6 +215,15 @@ async def get_house_metadata(lat: float, lon: float, db: Session) -> HouseMetada оценка обязана работать даже если картография недоступна. """ try: + # #393: кадастр зданий (Росреестр, cad_buildings) — приоритетный источник. + cad = _cad_buildings_get(db, lat, lon) + if cad is not None: + logger.info( + "house_metadata: cad_buildings (%.5f, %.5f) → year=%s floors=%s", + lat, lon, cad.year_built, cad.total_floors, + ) + return cad + cached = _cache_get(db, lat, lon) if cached is not None: logger.info("house_metadata cache hit: (%.5f, %.5f)", lat, lon) diff --git a/tradein-mvp/backend/data/sql/006_cad_buildings.sql b/tradein-mvp/backend/data/sql/006_cad_buildings.sql new file mode 100644 index 00000000..98589ec4 --- /dev/null +++ b/tradein-mvp/backend/data/sql/006_cad_buildings.sql @@ -0,0 +1,34 @@ +-- Trade-In MVP — кадастр зданий ЕКБ (#393). +-- Срез cad_buildings из gendesign-БД (данные Росреестра по зданиям), +-- импортируется ETL-ом deploy/import-cadastre.sh. +-- Основной источник обогащения house_metadata (год / этажность) — +-- данные точнее и полнее, чем OSM Overpass. + +BEGIN; + +CREATE TABLE IF NOT EXISTS cad_buildings ( + cad_num text PRIMARY KEY, -- кадастровый номер здания, 66:41:... + address text, + floors int, + year_built int, + area_m2 numeric(12, 2), -- площадь здания (не квартиры) + purpose text, -- Жилое / Нежилое / Многоквартирный дом + lat double precision, + lon double precision, + geom geometry(Point, 4326), + imported_at timestamptz NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS cad_buildings_geom_idx ON cad_buildings USING GIST (geom); + +-- Авто-update geom из (lat, lon) — переиспользуем функцию из 002_core_tables.sql. +DROP TRIGGER IF EXISTS cad_buildings_set_geom_trg ON cad_buildings; +CREATE TRIGGER cad_buildings_set_geom_trg + BEFORE INSERT OR UPDATE OF lat, lon ON cad_buildings + FOR EACH ROW EXECUTE FUNCTION listings_set_geom(); + +COMMENT ON TABLE cad_buildings IS + '#393 — кадастр зданий ЕКБ (срез cad_buildings из gendesign-БД). ' + 'Приоритетный источник обогащения house_metadata: год постройки / этажность.'; + +COMMIT; diff --git a/tradein-mvp/deploy/import-cadastre.sh b/tradein-mvp/deploy/import-cadastre.sh new file mode 100755 index 00000000..9764fab3 --- /dev/null +++ b/tradein-mvp/deploy/import-cadastre.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Импорт кадастра зданий ЕКБ из gendesign-БД в tradein cad_buildings (#393). +# +# Источник: gendesign-postgres-1 / cad_buildings (~37k зданий, 36k по ЕКБ). +# Берём ЕКБ (cad_num LIKE '66:41:%') с годом постройки или этажностью. +# Координаты — центроид geom. Используется как источник обогащения +# house_metadata (год / этажность) — точнее OSM. +# +# Запуск на прод-хосте: ./import-cadastre.sh +# Повторяемо: upsert по cad_num (PRIMARY KEY), новый запуск обновит данные. + +set -euo pipefail + +SRC_PG="${SRC_PG:-gendesign-postgres-1}" +DST_PG="${DST_PG:-tradein-postgres}" +SRC_DB="${SRC_DB:-gendesign}" +SRC_USER="${SRC_USER:-gendesign}" + +echo "[$(date -u +%H:%M:%S)] import-cadastre: здания ЕКБ из cad_buildings" + +# 1. Staging-таблица в tradein. +docker exec "$DST_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -c " + CREATE TABLE IF NOT EXISTS cad_buildings_staging ( + cad_num text PRIMARY KEY, address text, floors int, year_built int, + area_m2 numeric, purpose text, lat double precision, lon double precision + ); + TRUNCATE cad_buildings_staging; +" + +# 2. Трансформируем на gendesign-стороне, пайпим CSV в staging. +# Центроид geom → lat/lon (ST_Transform на 4326 на случай иного SRID). +docker exec "$SRC_PG" psql -U "$SRC_USER" -d "$SRC_DB" -v ON_ERROR_STOP=on -c " + COPY ( + SELECT + cad_num, + NULLIF(trim(readable_address), '') AS address, + NULLIF(floors, 0) AS floors, + CASE WHEN year_built BETWEEN 1800 AND 2100 + THEN year_built END AS year_built, + round(COALESCE(build_record_area, area), 2) AS area_m2, + purpose, + ST_Y(ST_Centroid(ST_Transform(geom, 4326))) AS lat, + ST_X(ST_Centroid(ST_Transform(geom, 4326))) AS lon + FROM cad_buildings + WHERE cad_num LIKE '66:41:%' + AND cad_num IS NOT NULL + AND (year_built IS NOT NULL OR floors IS NOT NULL) + ) TO STDOUT WITH CSV +" | docker exec -i "$DST_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -c " + COPY cad_buildings_staging FROM STDIN WITH CSV +" + +STAGED=$(docker exec "$DST_PG" psql -U tradein -d tradein -tAc \ + "SELECT count(*) FROM cad_buildings_staging") +echo "[$(date -u +%H:%M:%S)] staged: $STAGED зданий" + +# 3. Upsert в cad_buildings (trigger проставит geom из lat/lon). +docker exec "$DST_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -c " + INSERT INTO cad_buildings + (cad_num, address, floors, year_built, area_m2, purpose, lat, lon) + SELECT cad_num, address, floors, year_built, area_m2, purpose, lat, lon + FROM cad_buildings_staging + ON CONFLICT (cad_num) DO UPDATE SET + address = EXCLUDED.address, + floors = EXCLUDED.floors, + year_built = EXCLUDED.year_built, + area_m2 = EXCLUDED.area_m2, + purpose = EXCLUDED.purpose, + lat = EXCLUDED.lat, + lon = EXCLUDED.lon, + imported_at = NOW(); + + DROP TABLE cad_buildings_staging; +" + +TOTAL=$(docker exec "$DST_PG" psql -U tradein -d tradein -tAc \ + "SELECT count(*) FROM cad_buildings") +echo "[$(date -u +%H:%M:%S)] cad_buildings всего: $TOTAL" -- 2.45.3