SPB Git forge

spb/fabri-ka

Public

Agrégateur de produits québécois — www.fabri-ka.com

217commits 1branches 0releases
66.1 MBsize
maindefault branch
3 h agolast push
Python 38.4% HTML 30.3% TypeScript 17.2% CSS 11% JavaScript 3.2%
4.3 KB · 115 lines python
Raw Blame History
1#!/usr/bin/env python32"""Phase 2 — géocodage massif des boutiques via leur RTA (postal_prefix).34Le registre porte un préfixe postal (RTA, ex. « J0V ») pour 86,6 % des5boutiques mais aucune n'était géocodée. On géocode chaque RTA UNIQUE6(≈ 360) via Nominatim — politesse 1 req/s, cache disque7data/geocode_cache/fsa.json (seedé une fois, rejouable sans réseau) —8puis on propage ville approx. + lat/lng à toutes les boutiques porteuses.910Précision : centroïde de la RTA (quartier/secteur), pas l'adresse civique —11suffisant pour une carte régionale et le tri par proximité. La ville n'est12écrite que si Nominatim la fournit (aucune donnée inventée).1314Écrit : stores.city (si vide) / stores.lat / stores.lng en DB15        + champ "geo" {lat,lng,city,precision:"fsa"} dans data/stores.json.1617Usage : .venv/bin/python scripts/geocode_stores.py [--dry-run] [--max-requests N]18"""19import argparse20import json21import os22import sys23import time2425import requests2627ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))28sys.path.insert(0, ROOT)29CACHE_DIR = os.path.join(ROOT, "data", "geocode_cache")30os.makedirs(CACHE_DIR, exist_ok=True)31CACHE = os.path.join(CACHE_DIR, "fsa.json")3233from fabrika import db as fdb  # noqa: E4023435# Nominatim ne résout PAS les RTA seules (postalcode=H2X -> []), constaté le36# 2026-08-19. zippopotam.us les résout nativement pour le Canada (centroïde +37# nom du secteur). Même politesse : 1 req/s, cache disque.38ZIPPO = "https://api.zippopotam.us/CA/{fsa}"39HDRS = {"User-Agent": "FabriKaBot/1.0 (+https://www.fabri-ka.com/bot; contact@spboucher.ai)"}404142def geocode_fsa(fsa: str) -> dict | None:43    """RTA -> {lat, lng, city} via zippopotam.us (données GeoNames)."""44    r = requests.get(ZIPPO.format(fsa=fsa), headers=HDRS, timeout=20)45    if r.status_code == 404:46        return None47    r.raise_for_status()48    data = r.json()49    places = data.get("places") or []50    if not places:51        return None52    p = places[0]53    if (p.get("state abbreviation") or "") not in ("QC", ""):54        return None            # garde-fou : rester au Québec55    return {"lat": round(float(p["latitude"]), 5),56            "lng": round(float(p["longitude"]), 5),57            "city": (p.get("place name") or "").strip()}585960def main():61    ap = argparse.ArgumentParser()62    ap.add_argument("--dry-run", action="store_true")63    ap.add_argument("--max-requests", type=int, default=400)64    args = ap.parse_args()6566    reg_path = os.path.join(ROOT, "data", "stores.json")67    reg = json.load(open(reg_path))68    fsas = sorted({(s.get("postal_prefix") or "").strip().upper()[:3]69                   for s in reg["stores"] if s.get("postal_prefix")})70    cache = json.load(open(CACHE)) if os.path.exists(CACHE) else {}71    todo = [f for f in fsas if f not in cache][:args.max_requests]72    print(f"[geocode] {len(fsas)} RTA uniques, {len(todo)} à résoudre "73          f"(cache: {len(cache)})", flush=True)7475    n_req = 076    for fsa in todo:77        try:78            cache[fsa] = geocode_fsa(fsa)79        except Exception as exc:80            print(f"  ! {fsa}: {exc}", flush=True)81        n_req += 182        if n_req % 25 == 0:83            print(f"  {n_req}/{len(todo)}", flush=True)84            json.dump(cache, open(CACHE, "w"))85        time.sleep(1.1)          # politesse Nominatim : 1 req/s max86    json.dump(cache, open(CACHE, "w"))87    resolved = {k: v for k, v in cache.items() if v}88    print(f"[geocode] RTA résolues : {len(resolved)}/{len(fsas)} "89          f"({n_req} requêtes réseau)")9091    if args.dry_run:92        return9394    n_store = 095    con = fdb.connect()96    for s in reg["stores"]:97        fsa = (s.get("postal_prefix") or "").strip().upper()[:3]98        geo = resolved.get(fsa)99        if not geo:100            continue101        s["geo"] = {"lat": geo["lat"], "lng": geo["lng"],102                    "city": geo["city"], "precision": "fsa"}103        con.execute("UPDATE stores SET lat=?, lng=?, "104                    "city=CASE WHEN COALESCE(city,'')='' THEN ? ELSE city END "105                    "WHERE id=?",106                    (geo["lat"], geo["lng"], geo["city"], s["id"]))107        n_store += 1108    con.commit(); con.close()109    json.dump(reg, open(reg_path, "w"), ensure_ascii=False, indent=1)110    print(f"[geocode] boutiques géocodées : {n_store}")111112113if __name__ == "__main__":114    main()115