Agrégateur de produits québécois — www.fabri-ka.com
Python 38.4%
HTML 30.3%
TypeScript 17.2%
CSS 11%
JavaScript 3.2%
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