| 256 |
256 |
"unique_addresses": len(groupes), "api_requests": requests_made} |
| 257 |
257 |
print(f"[lou-ka] geocode {stats}") |
| 258 |
258 |
return stats |
|
259 |
+ |
|
260 |
+ |
|
261 |
+# -------------------------------------------------------------------------- |
|
262 |
+# Géocodage EN LOT (Adresses Québec `geocodeAddresses`) — porté d'Immo-Ka. |
|
263 |
+# ~40 immeubles/requête POST au lieu de 1 adresse/1,1 s : peuple la carte |
|
264 |
+# Lou-Ka Maps (framework Ka Maps) en minutes plutôt qu'en heures. |
|
265 |
+# -------------------------------------------------------------------------- |
|
266 |
+AQ_BATCH_URL = ("https://servicescarto.mern.gouv.qc.ca/pes/rest/services/Territoire/" |
|
267 |
+ "Adresse_Geocodage/GeocodeServer/geocodeAddresses") |
|
268 |
+BATCH_SIZE = 200 |
|
269 |
+ |
|
270 |
+ |
|
271 |
+def run_batch(limit: int | None = None) -> dict: |
|
272 |
+ """Géocode toutes les annonces sans coordonnées, par lots de 200 adresses |
|
273 |
+ normalisées (1 entrée par immeuble — le cache sert toutes les unités).""" |
|
274 |
+ import json as _json |
|
275 |
+ import sqlite3 as _sqlite3 |
|
276 |
+ |
|
277 |
+ con = db.connect() |
|
278 |
+ geo = Geocoder(con) # réutilise _clean / bbox |
|
279 |
+ rows = con.execute( |
|
280 |
+ """SELECT uid, address, city FROM listings |
|
281 |
+ WHERE active=1 AND lat IS NULL AND address IS NOT NULL |
|
282 |
+ AND address<>'' AND geocode_failed=0 ORDER BY address""").fetchall() |
|
283 |
+ uniq: dict[str, dict] = {} |
|
284 |
+ for r in rows: |
|
285 |
+ k = norm_key(r["address"]) |
|
286 |
+ if not k or len(k) < 6: |
|
287 |
+ continue |
|
288 |
+ u = uniq.setdefault(k, {"address": r["address"], "city": r["city"], |
|
289 |
+ "members": []}) |
|
290 |
+ u["members"].append(r["uid"]) |
|
291 |
+ pending = [] |
|
292 |
+ for k, u in uniq.items(): |
|
293 |
+ c = con.execute("SELECT lat,lng,failed FROM geocode_cache WHERE address=?", |
|
294 |
+ (k,)).fetchone() |
|
295 |
+ if c is not None and not c["failed"]: |
|
296 |
+ for uid in u["members"]: |
|
297 |
+ con.execute("UPDATE listings SET lat=?, lng=? WHERE uid=?", |
|
298 |
+ (c["lat"], c["lng"], uid)) |
|
299 |
+ continue |
|
300 |
+ if c is not None and c["failed"]: |
|
301 |
+ continue |
|
302 |
+ pending.append((k, u)) |
|
303 |
+ con.commit() |
|
304 |
+ if limit is not None: |
|
305 |
+ pending = pending[:limit] |
|
306 |
+ |
|
307 |
+ session = requests.Session() |
|
308 |
+ session.headers["User-Agent"] = USER_AGENT |
|
309 |
+ done = failed = 0 |
|
310 |
+ for i in range(0, len(pending), BATCH_SIZE): |
|
311 |
+ chunk = pending[i:i + BATCH_SIZE] |
|
312 |
+ records = {"records": [ |
|
313 |
+ {"attributes": {"OBJECTID": j, |
|
314 |
+ "SingleLine": f"{geo._clean(u['address']).split(',')[0].strip()}, " |
|
315 |
+ f"{(u['city'] or 'Québec').strip()}"}} |
|
316 |
+ for j, (_k, u) in enumerate(chunk)]} |
|
317 |
+ try: |
|
318 |
+ resp = session.post(AQ_BATCH_URL, data={ |
|
319 |
+ "addresses": _json.dumps(records, ensure_ascii=False), |
|
320 |
+ "f": "json", "outSR": 4326}, timeout=90) |
|
321 |
+ locs = resp.json().get("locations", []) |
|
322 |
+ except Exception as e: |
|
323 |
+ print(f"[lou-ka] geocode-batch lot {i//BATCH_SIZE} ERREUR: {str(e)[:80]}") |
|
324 |
+ time.sleep(1.0) |
|
325 |
+ continue |
|
326 |
+ by_id = {l["attributes"].get("ResultID"): l for l in locs} |
|
327 |
+ for j, (k, u) in enumerate(chunk): |
|
328 |
+ loc = by_id.get(j) |
|
329 |
+ coords = None |
|
330 |
+ if loc and loc["attributes"].get("Score", 0) >= AQ_MIN_SCORE: |
|
331 |
+ lc = loc.get("location") or {} |
|
332 |
+ try: |
|
333 |
+ cand = (float(lc["y"]), float(lc["x"])) |
|
334 |
+ if _in_bbox(*cand, _bbox_for(u["city"] or "")): |
|
335 |
+ coords = cand |
|
336 |
+ except (KeyError, ValueError, TypeError): |
|
337 |
+ coords = None |
|
338 |
+ try: |
|
339 |
+ if coords: |
|
340 |
+ for uid in u["members"]: |
|
341 |
+ con.execute("UPDATE listings SET lat=?, lng=? WHERE uid=?", |
|
342 |
+ (coords[0], coords[1], uid)) |
|
343 |
+ con.execute( |
|
344 |
+ "INSERT INTO geocode_cache (address,lat,lng,provider,failed,ts)" |
|
345 |
+ " VALUES (?,?,?,?,0,?) ON CONFLICT(address) DO UPDATE SET" |
|
346 |
+ " lat=excluded.lat, lng=excluded.lng, provider=excluded.provider," |
|
347 |
+ " failed=0, ts=excluded.ts", |
|
348 |
+ (k, coords[0], coords[1], "adresses_quebec_batch", time.time())) |
|
349 |
+ done += len(u["members"]) |
|
350 |
+ else: |
|
351 |
+ for uid in u["members"]: |
|
352 |
+ con.execute("UPDATE listings SET geocode_failed=1 WHERE uid=?", (uid,)) |
|
353 |
+ con.execute( |
|
354 |
+ "INSERT INTO geocode_cache (address,lat,lng,provider,failed,ts)" |
|
355 |
+ " VALUES (?,?,?,?,1,?) ON CONFLICT(address) DO UPDATE SET" |
|
356 |
+ " failed=1, ts=excluded.ts", |
|
357 |
+ (k, None, None, "adresses_quebec_batch", time.time())) |
|
358 |
+ failed += len(u["members"]) |
|
359 |
+ con.commit() |
|
360 |
+ except _sqlite3.OperationalError: |
|
361 |
+ try: |
|
362 |
+ con.rollback() |
|
363 |
+ except _sqlite3.Error: |
|
364 |
+ pass |
|
365 |
+ time.sleep(1.0) |
|
366 |
+ print(f"[lou-ka] geocode-batch {i+len(chunk)}/{len(pending)} " |
|
367 |
+ f"(résolues {done}, échecs {failed})") |
|
368 |
+ con.close() |
|
369 |
+ stats = {"geocoded": done, "failed": failed, |
|
370 |
+ "batches": (len(pending)+BATCH_SIZE-1)//BATCH_SIZE} |
|
371 |
+ print(f"[lou-ka] geocode-batch {stats}") |
|
372 |
+ return stats |