fix(c21): Saskatchewan sans polygone API -> recherche par ~30 villes
L API MoxiWorks n a pas de GeoLayer pour la province de Saskatchewan (« Unable to locate GeoLayer ») : le connecteur accepte desormais une liste de localites (attribut locations) et C21_SK interroge les ~30 principales villes SK. Registre canada_agencies restaure (4 sites RP re-fusionnes, hanlon page_size 10).
3 changed files +113 −14
modified
data/canada_agencies.json
+37 −0
@@ -107,5 +107,42 @@ | ||
| 107 | 107 | "site": "https://cbnorthbay.com", |
| 108 | 108 | "note": "Recensement ON 2026-08-27 : ~307 fiches (North Bay). RealtyPress/DDF.", |
| 109 | 109 | "province": "Ontario" |
| 110 | + }, | |
| 111 | + { | |
| 112 | + "id": "rp_ag_hanlonrealty", | |
| 113 | + "name": "hanlonrealty.ca (Newfoundland and Labrador)", | |
| 114 | + "site": "https://hanlonrealty.ca", | |
| 115 | + "archive": "listing", | |
| 116 | + "max_pages": 2400, | |
| 117 | + "province": "Newfoundland and Labrador", | |
| 118 | + "note": "Recensement CANADA 2026-08-27 : ~22 000 fiches (board NL complet). posts_per_page plafonne a ~3 500 items -> pagination NATIVE 10/page. RealtyPress/DDF.", | |
| 119 | + "page_size": 10 | |
| 120 | + }, | |
| 121 | + { | |
| 122 | + "id": "rp_ag_denisedunnrealtor", | |
| 123 | + "name": "denisedunnrealtor.com (British Columbia)", | |
| 124 | + "site": "https://denisedunnrealtor.com", | |
| 125 | + "archive": "listing", | |
| 126 | + "max_pages": 400, | |
| 127 | + "province": "British Columbia", | |
| 128 | + "note": "Recensement CANADA 2026-08-27 : ~≥2400 fiches, provinces page 1 : {'British Columbia': 24}. RealtyPress/DDF." | |
| 129 | + }, | |
| 130 | + { | |
| 131 | + "id": "rp_ag_reddoorrealty", | |
| 132 | + "name": "reddoorrealty.ca (Nova Scotia)", | |
| 133 | + "site": "https://reddoorrealty.ca", | |
| 134 | + "archive": "listing", | |
| 135 | + "max_pages": 10, | |
| 136 | + "province": "Nova Scotia", | |
| 137 | + "note": "Recensement CANADA 2026-08-27 : ~≥580 fiches, provinces page 1 : {'Nova Scotia': 202}. RealtyPress/DDF." | |
| 138 | + }, | |
| 139 | + { | |
| 140 | + "id": "rp_ag_raymondanthony", | |
| 141 | + "name": "raymondanthony.com (Alberta)", | |
| 142 | + "site": "https://raymondanthony.com", | |
| 143 | + "archive": "listing", | |
| 144 | + "max_pages": 400, | |
| 145 | + "province": "Alberta", | |
| 146 | + "note": "Recensement CANADA 2026-08-27 : ~≥60 fiches, provinces page 1 : {'Alberta': 25}. RealtyPress/DDF." | |
| 110 | 147 | } |
| 111 | 148 | ] |
| \ No newline at end of file | ||
modified
data/sources.json
+44 −0
@@ -285,6 +285,50 @@ | ||
| 285 | 285 | "status": "actif", |
| 286 | 286 | "type": "reseau", |
| 287 | 287 | "note": "API MoxiWorks corporative de c21.ca (svc.moxiworks.com listing/search_v2, JSONP). Paramètres d'attribution obligatoires, découpage adaptatif par tranches de prix (liste retournée seulement si number_found ≤ ~6k). Réponse liste complète : AUCUNE passe détail nécessaire. Rétro-conçue 2026-08-27." |
| 288 | + }, | |
| 289 | + { | |
| 290 | + "id": "rp_ag_hanlonrealty", | |
| 291 | + "name": "hanlonrealty.ca (Newfoundland and Labrador)", | |
| 292 | + "url": "https://hanlonrealty.ca", | |
| 293 | + "listing_url": "https://hanlonrealty.ca/listing/", | |
| 294 | + "coverage": "Newfoundland and Labrador — ~≥22000 fiches DDF", | |
| 295 | + "connector": "realtypress", | |
| 296 | + "status": "actif", | |
| 297 | + "type": "agence", | |
| 298 | + "note": "House-Ka — generic RealtyPress connector (registry data/canada_agencies.json). WordPress RealtyPress plugin on the CREA DDF feed. external_id ddf<id>: cross-site dedup by MIN(uid)." | |
| 299 | + }, | |
| 300 | + { | |
| 301 | + "id": "rp_ag_denisedunnrealtor", | |
| 302 | + "name": "denisedunnrealtor.com (British Columbia)", | |
| 303 | + "url": "https://denisedunnrealtor.com", | |
| 304 | + "listing_url": "https://denisedunnrealtor.com/listing/", | |
| 305 | + "coverage": "British Columbia — ~≥2400 fiches DDF", | |
| 306 | + "connector": "realtypress", | |
| 307 | + "status": "actif", | |
| 308 | + "type": "agence", | |
| 309 | + "note": "House-Ka — generic RealtyPress connector (registry data/canada_agencies.json). WordPress RealtyPress plugin on the CREA DDF feed. external_id ddf<id>: cross-site dedup by MIN(uid)." | |
| 310 | + }, | |
| 311 | + { | |
| 312 | + "id": "rp_ag_reddoorrealty", | |
| 313 | + "name": "reddoorrealty.ca (Nova Scotia)", | |
| 314 | + "url": "https://reddoorrealty.ca", | |
| 315 | + "listing_url": "https://reddoorrealty.ca/listing/", | |
| 316 | + "coverage": "Nova Scotia — ~≥580 fiches DDF", | |
| 317 | + "connector": "realtypress", | |
| 318 | + "status": "actif", | |
| 319 | + "type": "agence", | |
| 320 | + "note": "House-Ka — generic RealtyPress connector (registry data/canada_agencies.json). WordPress RealtyPress plugin on the CREA DDF feed. external_id ddf<id>: cross-site dedup by MIN(uid)." | |
| 321 | + }, | |
| 322 | + { | |
| 323 | + "id": "rp_ag_raymondanthony", | |
| 324 | + "name": "raymondanthony.com (Alberta)", | |
| 325 | + "url": "https://raymondanthony.com", | |
| 326 | + "listing_url": "https://raymondanthony.com/listing/", | |
| 327 | + "coverage": "Alberta — ~≥60 fiches DDF", | |
| 328 | + "connector": "realtypress", | |
| 329 | + "status": "actif", | |
| 330 | + "type": "agence", | |
| 331 | + "note": "House-Ka — generic RealtyPress connector (registry data/canada_agencies.json). WordPress RealtyPress plugin on the CREA DDF feed. external_id ddf<id>: cross-site dedup by MIN(uid)." | |
| 288 | 332 | } |
| 289 | 333 | ] |
| 290 | 334 | } |
| \ No newline at end of file | ||
modified
immoka/connectors/c21_canada.py
+32 −14
@@ -71,13 +71,18 @@ class _C21Province(BaseConnector): | ||
| 71 | 71 | |
| 72 | 72 | province = "" # libellé exact pour location_search_field |
| 73 | 73 | state_code = "" # code à 2 lettres attendu dans location.state |
| 74 | + locations: tuple = () # localités interrogées (défaut : la province) — | |
| 75 | + # l'API n'a PAS de polygone pour certaines | |
| 76 | + # provinces (Saskatchewan : « Unable to locate | |
| 77 | + # GeoLayer ») → liste de villes à la place | |
| 74 | 78 | request_delay = 0.35 |
| 75 | 79 | |
| 76 | 80 | # ------------------------------------------------------------------ API -- |
| 77 | 81 | def _query(self, pricemin: int | None, pricemax: int | None, |
| 78 | − startidx: int = 0, pgsize: int = PGSIZE) -> dict: | |
| 82 | + startidx: int = 0, pgsize: int = PGSIZE, | |
| 83 | + location: str | None = None) -> dict: | |
| 79 | 84 | params = dict(BASE_PARAMS) |
| 80 | − params["location_search_field"] = self.province | |
| 85 | + params["location_search_field"] = location or self.province | |
| 81 | 86 | params["pgsize"] = str(pgsize) |
| 82 | 87 | params["startidx"] = str(startidx) |
| 83 | 88 | if pricemin is not None and pricemin > 0: |
@@ -91,18 +96,20 @@ class _C21Province(BaseConnector): | ||
| 91 | 96 | raise RuntimeError(f"c21 API: {d.get('message', d.get('status'))}") |
| 92 | 97 | return d["data"] |
| 93 | 98 | |
| 94 | − def _count(self, lo: int | None, hi: int | None) -> int: | |
| 95 | − d = self._query(lo, hi, startidx=0, pgsize=1) | |
| 99 | + def _count(self, lo: int | None, hi: int | None, | |
| 100 | + location: str | None = None) -> int: | |
| 101 | + d = self._query(lo, hi, startidx=0, pgsize=1, location=location) | |
| 96 | 102 | return int(d.get("number_found") or 0) |
| 97 | 103 | |
| 98 | 104 | # ------------------------------------------------------ tranches de prix -- |
| 99 | − def _slices(self) -> list[tuple[int | None, int | None]]: | |
| 105 | + def _slices(self, location: str | None = None | |
| 106 | + ) -> list[tuple[int | None, int | None]]: | |
| 100 | 107 | """Tranches [lo, hi] dont number_found ≤ SLICE_MAX (scission dichotomique).""" |
| 101 | 108 | out: list[tuple[int | None, int | None]] = [] |
| 102 | 109 | stack: list[tuple[int, int]] = [(0, PRICE_CEIL)] |
| 103 | 110 | while stack: |
| 104 | 111 | lo, hi = stack.pop() |
| 105 | − n = self._count(lo or None, hi) | |
| 112 | + n = self._count(lo or None, hi, location=location) | |
| 106 | 113 | time.sleep(self.request_delay) |
| 107 | 114 | if n == 0: |
| 108 | 115 | continue |
@@ -189,13 +196,14 @@ class _C21Province(BaseConnector): | ||
| 189 | 196 | # --------------------------------------------------------------- fetch -- |
| 190 | 197 | def fetch(self) -> list[PropertyListing]: |
| 191 | 198 | by_id: dict[str, PropertyListing] = {} |
| 192 | − for lo, hi in self._slices(): | |
| 193 | − d = self._query(lo, hi, pgsize=PGSIZE) | |
| 194 | − for r in (d.get("result_list") or []): | |
| 195 | − lst = self._to_listing(r) | |
| 196 | − if lst is not None and lst.external_id not in by_id: | |
| 197 | − by_id[lst.external_id] = lst | |
| 198 | − time.sleep(self.request_delay) | |
| 199 | + for loc in (self.locations or (None,)): | |
| 200 | + for lo, hi in self._slices(location=loc): | |
| 201 | + d = self._query(lo, hi, pgsize=PGSIZE, location=loc) | |
| 202 | + for r in (d.get("result_list") or []): | |
| 203 | + lst = self._to_listing(r) | |
| 204 | + if lst is not None and lst.external_id not in by_id: | |
| 205 | + by_id[lst.external_id] = lst | |
| 206 | + time.sleep(self.request_delay) | |
| 199 | 207 | return list(by_id.values()) |
| 200 | 208 | |
| 201 | 209 | |
@@ -214,9 +222,19 @@ _PROVINCES = [ | ||
| 214 | 222 | ("c21_nt", "Northwest Territories", "NT"), |
| 215 | 223 | ] |
| 216 | 224 | |
| 225 | +# Saskatchewan : pas de polygone provincial côté API → villes principales | |
| 226 | +_SK_CITIES = tuple(f"{c}, SK" for c in ( | |
| 227 | + "Saskatoon", "Regina", "Prince Albert", "Moose Jaw", "Swift Current", | |
| 228 | + "Yorkton", "North Battleford", "Estevan", "Weyburn", "Warman", | |
| 229 | + "Martensville", "Melfort", "Humboldt", "Meadow Lake", "Kindersley", | |
| 230 | + "Melville", "Nipawin", "Tisdale", "La Ronge", "Battleford", "Rosetown", | |
| 231 | + "Assiniboia", "Unity", "Canora", "Esterhazy", "Outlook", "Watrous", | |
| 232 | + "Moosomin", "Shellbrook", "Fort Qu'Appelle")) | |
| 233 | + | |
| 217 | 234 | for _sid, _prov, _code in _PROVINCES: |
| 235 | + _extra = {"locations": _SK_CITIES} if _code == "SK" else {} | |
| 218 | 236 | globals()[f"C21_{_code}"] = type( |
| 219 | 237 | f"C21{_code}", |
| 220 | 238 | (_C21Province,), |
| 221 | − {"source_id": _sid, "province": _prov, "state_code": _code}, | |
| 239 | + {"source_id": _sid, "province": _prov, "state_code": _code, **_extra}, | |
| 222 | 240 | ) |
| 223 | 241 | |