gendesign/site-finder/12_more_pois.py
Light1YT 97b19a0b85 Import Site Finder app from analysis/ vibe-coding session
Adds site-finder/ subfolder with:
  - server.py — FastAPI scoring service v2 (35 endpoints, ~85KB)
  - 01_load_sites.py … 12_more_pois.py — data ingest pipeline
  - db_init.py — SQLite schema bootstrap
  - static/ — Leaflet UI (index.html ~3500 lines + sw.js)
  - cache/ — small persistent caches (admin districts, jk polygons,
    geocode warm cache, parcel polygons drop-zone with README)
  - reports/ — sample generated parcel report (HTML+JSON)

Excluded via .gitignore (regeneratable, too big for git):
  - analysis.db (336MB SQLite — rebuild via 01_*..12_*.py)
  - cache/objective_raw/ (1.2GB Объектив raw dumps)
  - cache/overpass_raw.json, cache/osm_buildings_all.geojson
    (regen from Overpass API)

Production deploy: /opt/gendesign/site-finder/ on gendsgn.ru
(container gendesign-site-finder-1, served at /sf/).
2026-05-10 22:42:25 +05:00

102 lines
4.1 KiB
Python

"""Add more POI categories: parking, gym/fitness, theater, taxi, marketplaces, etc."""
import sqlite3, pathlib, requests, time, json, math, urllib3
urllib3.disable_warnings()
DB = pathlib.Path(__file__).parent / "analysis.db"
CACHE = pathlib.Path(__file__).parent / "cache" / "overpass_raw.json"
NEW_QUERIES = [
("parking", '["amenity"="parking"]'),
("gym", '["leisure"="fitness_centre"]'),
("theater", '["amenity"="theatre"]'),
("cinema", '["amenity"="cinema"]'),
("marketplace", '["amenity"="marketplace"]'),
("museum", '["tourism"="museum"]'),
("hotel", '["tourism"~"^(hotel|hostel|apartment)$"]'),
("nightclub", '["amenity"~"^(nightclub|bar|pub)$"]'),
("car_wash", '["amenity"="car_wash"]'),
("car_rental", '["amenity"~"^(car_rental|taxi)$"]'),
("vet", '["amenity"="veterinary"]'),
("courier", '["amenity"~"^(parcel_locker|post_box)$"]'),
("kindergarten_priv", '["amenity"="kindergarten"]["fee"="yes"]'), # platnyye
("dentist", '["amenity"="dentist"]'),
("childcare", '["amenity"="childcare"]'),
]
ENDPOINTS = [
"https://overpass-api.de/api/interpreter",
"https://overpass.kumi.systems/api/interpreter",
]
def hav(la1,lo1,la2,lo2):
R=6371000;p1,p2=math.radians(la1),math.radians(la2)
dp=math.radians(la2-la1);dl=math.radians(lo2-lo1)
a=math.sin(dp/2)**2+math.cos(p1)*math.cos(p2)*math.sin(dl/2)**2
return 2*R*math.asin(math.sqrt(a))
def overpass(filt, b):
q = f'[out:json][timeout:120];(node{filt}({b[0]},{b[1]},{b[2]},{b[3]});way{filt}({b[0]},{b[1]},{b[2]},{b[3]}););out center tags;'
last = None
for ep in ENDPOINTS:
try:
r = requests.post(ep, data={"data": q}, timeout=180, verify=False,
headers={"User-Agent":"gendesign/0.7"})
if r.ok: return r.json().get("elements", [])
last = f"{ep} {r.status_code}"
time.sleep(2)
except Exception as e:
last = f"{ep} {e}"; time.sleep(2)
raise RuntimeError(last)
def main():
cache = json.loads(CACHE.read_text()) if CACHE.exists() else {}
conn = sqlite3.connect(DB)
sites = conn.execute("SELECT site_id, lat, lon FROM sites").fetchall()
lats=[s[1] for s in sites]; lons=[s[2] for s in sites]
b = (min(lats)-0.005, min(lons)-0.005, max(lats)+0.005, max(lons)+0.005)
for cat, filt in NEW_QUERIES:
if cat in cache:
print(f" {cat:<20} cached: {len(cache[cat])}")
continue
print(f" {cat:<20} fetching...", end=" ", flush=True)
try:
elems = overpass(filt, b)
cache[cat] = elems
print(f"{len(elems)}")
except Exception as e:
print(f"FAIL {e}")
cache[cat] = []
time.sleep(2)
CACHE.write_text(json.dumps(cache, ensure_ascii=False))
print(f"\nCache: {len(cache)} categories, {CACHE.stat().st_size/1024:.0f} KB")
# rebuild pois table
conn.execute("DELETE FROM pois")
n = 0
for cat, elems in cache.items():
for el in elems:
if el["type"] == "node":
la, lo = el.get("lat"), el.get("lon")
else:
c = el.get("center") or {}
la, lo = c.get("lat"), c.get("lon")
if la is None: continue
tags = el.get("tags") or {}
name = tags.get("name") or tags.get("operator") or ""
for sid, sla, slo in sites:
d = hav(sla, slo, la, lo)
if d <= 2000:
conn.execute("""INSERT INTO pois(site_id,category,osm_type,osm_id,name,lat,lon,distance_m,raw_tags)
VALUES (?,?,?,?,?,?,?,?,?)""",
(sid, cat, el["type"], str(el["id"]), name, la, lo, d, json.dumps(tags, ensure_ascii=False)))
n += 1
conn.commit()
print(f"recomputed {n} site-poi pairs")
print("\nTop categories:")
for c, n in conn.execute("SELECT category, count(*) FROM pois GROUP BY 1 ORDER BY 2 DESC LIMIT 25").fetchall():
print(f" {c:<20} {n}")
conn.close()
if __name__ == "__main__":
main()