Merge pull request 'feat(tradein): кадастр зданий как источник house_metadata (#393)' (#406) from feat/tradein-cadastre-enrichment into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 4s
Deploy Trade-In / build-frontend (push) Successful in 23s
Deploy Trade-In / build-backend (push) Successful in 40s
Deploy Trade-In / deploy (push) Successful in 15s

This commit is contained in:
lekss361 2026-05-22 05:55:05 +00:00
commit c86f2e5a72
3 changed files with 155 additions and 0 deletions

View file

@ -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)

View file

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

View file

@ -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"