feat(tradein): кадастр зданий как источник house_metadata (#393) #406
3 changed files with 155 additions and 0 deletions
|
|
@ -29,6 +29,7 @@ logger = logging.getLogger(__name__)
|
||||||
OVERPASS_URL = "https://overpass-api.de/api/interpreter"
|
OVERPASS_URL = "https://overpass-api.de/api/interpreter"
|
||||||
_CACHE_RADIUS_M = 40 # дом в пределах 40 м от точки — считаем тем же
|
_CACHE_RADIUS_M = 40 # дом в пределах 40 м от точки — считаем тем же
|
||||||
_OVERPASS_RADIUS_M = 25 # ищем здание в 25 м от геокодированной точки
|
_OVERPASS_RADIUS_M = 25 # ищем здание в 25 м от геокодированной точки
|
||||||
|
_CAD_RADIUS_M = 60 # радиус поиска здания в cad_buildings (#393)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|
@ -137,6 +138,39 @@ def _cache_put(db: Session, meta: HouseMetadata, raw: dict) -> None:
|
||||||
db.commit()
|
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 ───────────────────────────────────────────────────
|
# ── Provider: OSM Overpass ───────────────────────────────────────────────────
|
||||||
async def _overpass_lookup(lat: float, lon: float) -> tuple[HouseMetadata, dict] | None:
|
async def _overpass_lookup(lat: float, lon: float) -> tuple[HouseMetadata, dict] | None:
|
||||||
"""Запрос здания у точки через Overpass API. (meta, raw_payload) или None."""
|
"""Запрос здания у точки через Overpass API. (meta, raw_payload) или None."""
|
||||||
|
|
@ -181,6 +215,15 @@ async def get_house_metadata(lat: float, lon: float, db: Session) -> HouseMetada
|
||||||
оценка обязана работать даже если картография недоступна.
|
оценка обязана работать даже если картография недоступна.
|
||||||
"""
|
"""
|
||||||
try:
|
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)
|
cached = _cache_get(db, lat, lon)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
logger.info("house_metadata cache hit: (%.5f, %.5f)", lat, lon)
|
logger.info("house_metadata cache hit: (%.5f, %.5f)", lat, lon)
|
||||||
|
|
|
||||||
34
tradein-mvp/backend/data/sql/006_cad_buildings.sql
Normal file
34
tradein-mvp/backend/data/sql/006_cad_buildings.sql
Normal 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;
|
||||||
78
tradein-mvp/deploy/import-cadastre.sh
Executable file
78
tradein-mvp/deploy/import-cadastre.sh
Executable 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"
|
||||||
Loading…
Add table
Reference in a new issue