Merge pull request 'fix(tradein): реальные координаты Cian/N1 через геокодинг' (#379) from fix/tradein-real-coords into main
All checks were successful
Deploy Trade-In / build-backend (push) Successful in 45s
Deploy Trade-In / deploy (push) Successful in 16s
Deploy Trade-In / changes (push) Successful in 4s
Deploy Trade-In / build-frontend (push) Successful in 28s

This commit is contained in:
lekss361 2026-05-21 10:14:48 +00:00
commit 1d4b720991
4 changed files with 39 additions and 24 deletions

View file

@ -111,23 +111,36 @@ async def scrape_around(
)
def _clean_address_for_geocode(addr: str) -> str:
"""Чистим address для геокодера.
Cian отдаёт «улица Латвийская, 56/3 · р-н Чкаловский» суффикс ' · ...'
мешает Nominatim. Берём часть до ' · '. N1 отдаёт «Репина, 75/2 стр.» ок.
"""
main = addr.split(" · ")[0].strip()
return main or addr
@router.post("/geocode-missing")
async def geocode_missing(
db: Annotated[Session, Depends(get_db)],
limit: int = 50,
limit: int = 500,
) -> dict:
"""Геокодинг listings у которых нет lat/lon (используя address).
Запускать после scrape если парсер не достал координаты.
Nominatim rate-limited 1 req/sec поэтому небольшой batch.
Запускать после scrape Cian/N1 не отдают координаты, только адрес.
Nominatim rate-limited 1 req/sec большой batch идёт ~limit секунд.
geom обновляется автоматически триггером listings_set_geom_trg.
"""
rows = db.execute(
text(
"""
SELECT id, address, raw_payload
SELECT id, address
FROM listings
WHERE (lat IS NULL OR lon IS NULL)
AND COALESCE(address, '') != ''
AND address NOT LIKE '%(Avito)%'
AND address NOT LIKE '%(N1)%'
ORDER BY scraped_at DESC
LIMIT :limit
"""
@ -138,18 +151,14 @@ async def geocode_missing(
geocoded = 0
skipped = 0
for row in rows:
result = await geocode(row["address"], db)
clean = _clean_address_for_geocode(row["address"])
result = await geocode(clean, db)
if result is None:
skipped += 1
continue
# geom пересчитается триггером из lat/lon.
db.execute(
text(
"""
UPDATE listings
SET lat = :lat, lon = :lon
WHERE id = :id
"""
),
text("UPDATE listings SET lat = :lat, lon = :lon WHERE id = :id"),
{"lat": result.lat, "lon": result.lon, "id": row["id"]},
)
db.commit()

View file

@ -346,12 +346,12 @@ class CianScraper(BaseScraper):
if ppm2 > 0:
area = round(price / ppm2, 2)
# Coords: anchor + jitter (Cian не отдаёт координаты в card HTML, только в API)
# Coords: НЕ ставим (Cian card HTML не отдаёт). Оставляем None —
# geocode-missing проставит реальные по address. Anchor+jitter тут
# был БАГ: Cian отдаёт ВСЕ объявления ЕКБ независимо от anchor, и
# jitter вокруг одного якоря давал фейковые координаты → ST_DWithin
# не находил аналоги для адресов вдали от центра.
lat = lon = None
if hasattr(self, "_anchor_lat"):
jitter_deg = (self._anchor_radius_m / 1000) / 111
lat = self._anchor_lat + (random.random() - 0.5) * jitter_deg
lon = self._anchor_lon + (random.random() - 0.5) * jitter_deg / 2
# Photos из img.src в card (первые 5)
photo_urls: list[str] = []

View file

@ -12,13 +12,13 @@ SSR HTML с DOM-карточками `data-test="offers-list-item"`.
- [data-test="offers-list-item-param-price"] "15 700 000"
- img.src фото
Координаты карточки не отдаёт anchor+jitter (как Avito).
Координаты карточки не отдаёт оставляем lat/lon = None,
geocode-missing проставит реальные по адресу.
"""
from __future__ import annotations
import logging
import random
import re
from typing import Any
@ -167,12 +167,10 @@ class N1Scraper(BaseScraper):
if not price:
return None
# Координаты: anchor + jitter (N1 card не отдаёт coords)
# Координаты НЕ ставим — N1 card не отдаёт. Оставляем None,
# geocode-missing проставит реальные по address (N1 отдаёт всё ЕКБ,
# jitter вокруг якоря был бы фейком).
lat = lon = None
if hasattr(self, "_anchor_lat"):
jitter_deg = (self._anchor_radius_m / 1000) / 111
lat = self._anchor_lat + (random.random() - 0.5) * jitter_deg
lon = self._anchor_lon + (random.random() - 0.5) * jitter_deg / 2
# Фото
photos: list[str] = []

View file

@ -63,4 +63,12 @@ for anchor in "${ANCHORS[@]}"; do
sleep 5
done
# Геокодинг свежих лотов без координат (Cian/N1 отдают только адрес).
# Nominatim 1 req/sec → batch ~500 идёт до ~8 мин.
log "→ geocode-missing"
geo=$(docker exec "$CONTAINER" curl -s -X POST \
"http://localhost:8000/api/v1/admin/geocode-missing?limit=500" \
-m 700 2>/dev/null || echo '{"error":"geocode_timeout"}')
log "← geocode-missing $geo"
log "=== cron-scrape done ==="