SPB Git

spb/lou-ka Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

HTML 99.7%

Géocodage : requêtes Nominatim structurées (street/city/state) + repli centroïde de rue

La recherche libre confondait « Québec » ville/province (141 rue Saint-Paul
-> Repentigny, rejeté par la bbox). Stratégies en cascade : structurée
civique+rue, libre complète, rue seule. Double civique « 6275 et 6375 » géré.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simon-pierre boucher committed 2 days ago (Aug 8, 2026) parent 3c5c00a

Showing 1 changed file with +36 and −10

modified louka/geocode.py +36 −10
@@ -62,6 +62,8 @@ class Geocoder:
62 62 s = address.strip()
63 63 # « 101-2905 Rue X » = unité 101, civique 2905 -> garder le civique
64 64 s = re.sub(r"^(\d+)-(\d+)\s", r"\2 ", s)
65 + # « 6275 et 6375 boulevard X » -> premier civique
66 + s = re.sub(r"^(\d+)\s+et\s+\d+\s", r"\1 ", s)
65 67 # « Montréal - Laval » / « Montréal - Île-des-Soeurs » -> garder le vrai lieu
66 68 s = re.sub(r"montr[ée]al\s*-\s*", "", s, flags=re.I)
67 69 # abréviations cardinales : « Rue Salaberry O » -> « Ouest »
@@ -78,14 +80,14 @@ class Geocoder:
78 80 q += ", Québec"
79 81 return q + ", Canada"
80 82
81 def _query_nominatim(self, q: str) -> tuple[float, float] | None:
83 + def _query_nominatim(self, params: dict) -> tuple[float, float] | None:
82 84 """Une requête Nominatim, throttlée."""
83 85 wait = REQUEST_DELAY - (time.time() - self._last)
84 86 if wait > 0:
85 87 time.sleep(wait)
86 88 try:
87 89 resp = self.session.get(NOMINATIM_URL, params={
88 "q": q, "format": "jsonv2", "limit": 1, "countrycodes": "ca",
90 + "format": "jsonv2", "limit": 1, "countrycodes": "ca", **params,
89 91 }, timeout=20)
90 92 self._last = time.time()
91 93 resp.raise_for_status()
@@ -100,6 +102,30 @@ class Geocoder:
100 102 except (KeyError, ValueError):
101 103 return None
102 104
105 + def _attempts(self, address: str, city: str) -> list[dict]:
106 + """Stratégies de requête, de la plus précise à la moins précise.
107 +
108 + 1) structurée civique+rue (évite l'ambiguïté « Québec » ville/province)
109 + 2) recherche libre complète
110 + 3) structurée rue seule -> centroïde de rue (repli acceptable pour la
111 + carte quand le numéro civique est absent d'OpenStreetMap)
112 + """
113 + premier = self._clean(address).split(",")[0].strip()
114 + ville = (city or "").strip()
115 + if not ville:
116 + k = strip_accents(address.lower())
117 + ville = "Lévis" if "levis" in k else "Québec"
118 + commun = {"city": ville, "state": "Québec", "country": "Canada"}
119 +
120 + tries: list[dict] = []
121 + m = re.match(r"^(\d+)[,\s]+(.{4,})$", premier)
122 + if m:
123 + tries.append({"street": f"{m.group(1)} {m.group(2)}", **commun})
124 + tries.append({"q": self._build_query(address, city)})
125 + if m:
126 + tries.append({"street": m.group(2), **commun})
127 + return tries
128 +
103 129 def resolve(self, address: str, city: str) -> tuple[float, float] | None:
104 130 """Adresse -> (lat, lng), via cache puis Nominatim. None si introuvable."""
105 131 key = norm_key(address)
@@ -114,14 +140,14 @@ class Geocoder:
114 140 if time.time() - (row["ts"] or 0) < RETRY_FAILED_AFTER:
115 141 return None # échec récent : ne pas marteler l'API
116 142
117 coords = self._query_nominatim(self._build_query(address, city))
118 if coords is None:
119 # repli : civique + rue seulement (sans code postal ni secteur)
120 simple = self._clean(address).split(",")[0].strip()
121 if simple and re.match(r"^\d", simple):
122 q = f"{simple}, {city or 'Québec'}, Québec, Canada"
123 coords = self._query_nominatim(q)
124 ok = coords is not None and _in_bbox(*coords, _bbox_for(city))
143 + bbox = _bbox_for(city)
144 + coords = None
145 + for params in self._attempts(address, city):
146 + c = self._query_nominatim(params)
147 + if c is not None and _in_bbox(*c, bbox):
148 + coords = c
149 + break
150 + ok = coords is not None
125 151 self.con.execute(
126 152 "INSERT INTO geocode_cache (address, lat, lng, provider, failed, ts)"
127 153 " VALUES (?,?,?,?,?,?)"
128 154