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/).
145 lines
5.6 KiB
Python
145 lines
5.6 KiB
Python
"""Fetch POIs from OSM Overpass for all sites in one bulk query.
|
|
|
|
POI taxonomy (matches the user's spec):
|
|
EDUCATION
|
|
kindergarten amenity=kindergarten
|
|
school amenity=school
|
|
university amenity=university | college
|
|
HEALTH
|
|
pharmacy amenity=pharmacy
|
|
clinic amenity=clinic | doctors
|
|
hospital amenity=hospital
|
|
RETAIL
|
|
shop_big shop=mall | supermarket | department_store | hypermarket
|
|
shop_med shop=convenience | grocery | bakery
|
|
shop_small shop=kiosk | newsagent
|
|
TRANSIT
|
|
bus_stop highway=bus_stop | public_transport=platform
|
|
tram_stop railway=tram_stop
|
|
metro railway=station + station=subway
|
|
LEISURE
|
|
park leisure=park | garden
|
|
playground leisure=playground
|
|
sports leisure=sports_centre | fitness_centre | pitch
|
|
"""
|
|
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"
|
|
|
|
OVERPASS_ENDPOINTS = [
|
|
"https://overpass-api.de/api/interpreter",
|
|
"https://overpass.kumi.systems/api/interpreter",
|
|
"https://overpass.openstreetmap.ru/api/interpreter",
|
|
]
|
|
|
|
# (category, overpass filter)
|
|
QUERIES = [
|
|
("kindergarten", '["amenity"="kindergarten"]'),
|
|
("school", '["amenity"="school"]'),
|
|
("university", '["amenity"~"^(university|college)$"]'),
|
|
("pharmacy", '["amenity"="pharmacy"]'),
|
|
("clinic", '["amenity"~"^(clinic|doctors)$"]'),
|
|
("hospital", '["amenity"="hospital"]'),
|
|
("shop_big", '["shop"~"^(mall|supermarket|department_store|hypermarket)$"]'),
|
|
("shop_med", '["shop"~"^(convenience|grocery|bakery)$"]'),
|
|
("shop_small", '["shop"~"^(kiosk|newsagent)$"]'),
|
|
("bus_stop", '["highway"="bus_stop"]'),
|
|
("tram_stop", '["railway"="tram_stop"]'),
|
|
("metro", '["station"="subway"]'),
|
|
("park", '["leisure"~"^(park|garden)$"]'),
|
|
("playground", '["leisure"="playground"]'),
|
|
("sports", '["leisure"~"^(sports_centre|fitness_centre|pitch)$"]'),
|
|
]
|
|
|
|
def haversine_m(lat1, lon1, lat2, lon2):
|
|
R = 6371000
|
|
p1, p2 = math.radians(lat1), math.radians(lat2)
|
|
dp = math.radians(lat2-lat1); dl = math.radians(lon2-lon1)
|
|
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 bbox(rows, pad_deg=0.05):
|
|
lats=[r[0] for r in rows]; lons=[r[1] for r in rows]
|
|
return (min(lats)-pad_deg, min(lons)-pad_deg, max(lats)+pad_deg, max(lons)+pad_deg)
|
|
|
|
def overpass_query(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_err = None
|
|
for ep in OVERPASS_ENDPOINTS:
|
|
try:
|
|
r = requests.post(ep, data={"data": q}, timeout=180, verify=False,
|
|
headers={"User-Agent":"gendesign-research/1.0"})
|
|
if r.status_code == 200:
|
|
return r.json()
|
|
last_err = f"HTTP {r.status_code} from {ep}"
|
|
time.sleep(2)
|
|
except Exception as e:
|
|
last_err = f"{ep}: {e}"
|
|
time.sleep(2)
|
|
raise RuntimeError(f"All Overpass endpoints failed: {last_err}")
|
|
|
|
def main():
|
|
conn = sqlite3.connect(DB)
|
|
sites = conn.execute("SELECT site_id, lat, lon FROM sites").fetchall()
|
|
print(f"Sites: {len(sites)}")
|
|
coords = [(s[1], s[2]) for s in sites]
|
|
b = bbox(coords)
|
|
print(f"BBox (S,W,N,E): {b}")
|
|
|
|
cache_data = {}
|
|
for cat, filt in QUERIES:
|
|
print(f" fetching {cat:<14} ...", end=" ", flush=True)
|
|
d = overpass_query(filt, b)
|
|
elements = d.get("elements", [])
|
|
cache_data[cat] = elements
|
|
print(f"{len(elements)} elements")
|
|
time.sleep(2) # be kind
|
|
|
|
CACHE.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(CACHE, 'w') as f: json.dump(cache_data, f, ensure_ascii=False)
|
|
print(f"\nCache: {CACHE} ({CACHE.stat().st_size/1024:.0f} KB)")
|
|
|
|
# Compute nearest POI per site per category, plus all POIs within 1km
|
|
conn.execute("DELETE FROM pois")
|
|
n_pois = 0
|
|
for cat, elems in cache_data.items():
|
|
# extract (lat,lon,name,raw)
|
|
poi_list = []
|
|
for el in elems:
|
|
if el["type"] == "node":
|
|
lat, lon = el.get("lat"), el.get("lon")
|
|
else:
|
|
c = el.get("center") or {}
|
|
lat, lon = c.get("lat"), c.get("lon")
|
|
if lat is None or lon is None: continue
|
|
tags = el.get("tags") or {}
|
|
name = tags.get("name") or tags.get("operator") or ""
|
|
poi_list.append((lat, lon, name, el["type"], el["id"], json.dumps(tags, ensure_ascii=False)))
|
|
# for each site, find within 2 km (we'll bucket by distance later)
|
|
for s in sites:
|
|
site_id, slat, slon = s
|
|
for plat, plon, pname, ptype, pid, ptags in poi_list:
|
|
d = haversine_m(slat, slon, plat, plon)
|
|
if d <= 2000:
|
|
conn.execute("""INSERT INTO pois(site_id,category,osm_type,osm_id,name,lat,lon,distance_m,raw_tags)
|
|
VALUES (?,?,?,?,?,?,?,?,?)""",
|
|
(site_id, cat, ptype, str(pid), pname, plat, plon, d, ptags))
|
|
n_pois += 1
|
|
conn.commit()
|
|
print(f"\nStored {n_pois} POI-site pairs (within 2 km)")
|
|
print("Per-category:")
|
|
for cat, n in conn.execute("SELECT category,count(*) FROM pois GROUP BY 1 ORDER BY 2 DESC").fetchall():
|
|
print(f" {cat:<14} {n}")
|
|
conn.close()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|