| 1 |
1 |
# ----------------------------------------------------------------------------- |
| 2 |
|
−# Immo-Ka — Agrégateur de propriétés à vendre (province de Québec) |
|
2 |
+# House-Ka — Agrégateur de maisons à vendre (Canada hors Québec) |
| 3 |
3 |
# Auteur : Simon-Pierre Boucher — contact@spboucher.ai |
| 4 |
|
−# commerces.py : grands commerces à proximité — API Mapbox Search Box |
|
4 |
+# commerces.py : grands commerces à proximité — API Mapbox Search Box, |
|
5 |
+# bannières ADAPTÉES À CHAQUE PROVINCE. |
| 5 |
6 |
# |
| 6 |
|
−# Pour chaque grande bannière (Costco, Metro, IGA, Walmart…), on interroge |
| 7 |
|
−# l'API Search Box de Mapbox (jeton PUBLIC pk.… lu dans |
| 8 |
|
−# frontend/src/kamaps/config.ts — source de vérité du projet) avec la |
| 9 |
|
−# position de l'annonce en `proximity`, et on retient le point de vente le |
| 10 |
|
−# plus proche. Cache par cellule d'environ 1 km (data/commerces.db, |
| 11 |
|
−# TTL 30 jours) : les fiches d'un même secteur ne recoûtent rien. |
|
7 |
+# Pour chaque grande bannière du panier provincial, on interroge l'API |
|
8 |
+# Search Box de Mapbox (jeton PUBLIC pk.… lu dans |
|
9 |
+# frontend/src/kamaps/config.ts) avec la position de l'annonce en |
|
10 |
+# `proximity`, et on retient le point de vente le plus proche. Cache par |
|
11 |
+# cellule d'environ 1 km (data/commerces.db, TTL 30 jours). |
|
12 |
+# |
|
13 |
+# Paniers par province (2026-08) — fondés sur la présence réelle des |
|
14 |
+# chaînes : Loblaw n°1 national (Superstore/No Frills, Zehrs ON, Dominion NL), |
|
15 |
+# Sobeys n°2 (dominant en Atlantique : Sobeys/Foodland/Lawtons), Metro n°3 |
|
16 |
+# (Ontario+Québec, Food Basics), Pattison/Save-On-Foods ~180 magasins dans |
|
17 |
+# l'Ouest + Yukon, Co-op (FCL) ~300 magasins en Saskatchewan, monopoles |
|
18 |
+# d'alcool provinciaux (LCBO, BC Liquor, Liquor Mart MB, NB Liquor, NSLC), |
|
19 |
+# Kent (Irving) en quincaillerie atlantique, London Drugs (BC/AB), |
|
20 |
+# Colemans (chaîne terre-neuvienne). |
| 12 |
21 |
# ----------------------------------------------------------------------------- |
| 13 |
22 |
from __future__ import annotations |
| 14 |
23 |
|
| 28 |
37 |
TTL = 30 * 86400 |
| 29 |
38 |
API = "https://api.mapbox.com/search/searchbox/v1/forward" |
| 30 |
39 |
|
| 31 |
|
−# id, libellé, requête Mapbox, mot-clé de validation (le nom du POI doit le |
| 32 |
|
−# contenir, sans accents ni casse — écarte « Station Métro », « Super Qualité »…) |
| 33 |
|
−BRANDS = [ |
| 34 |
|
− ("costco", "Costco", "Costco Wholesale", "costco"), |
| 35 |
|
− ("walmart", "Walmart", "Walmart Supercentre", "walmart"), |
| 36 |
|
− ("metro", "Metro", "Metro", "metro"), |
| 37 |
|
− ("iga", "IGA", "IGA", "iga"), |
| 38 |
|
− ("maxi", "Maxi", "Maxi", "maxi"), |
| 39 |
|
− ("superc", "Super C", "Super C", "super c"), |
| 40 |
|
− ("provigo", "Provigo", "Provigo", "provigo"), |
| 41 |
|
− ("canadiantire", "Canadian Tire", "Canadian Tire", "canadian tire"), |
| 42 |
|
− ("dollarama", "Dollarama", "Dollarama", "dollarama"), |
| 43 |
|
− ("saq", "SAQ", "SAQ", "saq"), |
| 44 |
|
− ("pharmaprix", "Pharmaprix", "Pharmaprix", "pharmaprix"), |
| 45 |
|
− ("jeancoutu", "Jean Coutu", "Jean Coutu pharmacie", "jean coutu"), |
| 46 |
|
− ("homedepot", "Home Depot", "Home Depot", "home depot"), |
| 47 |
|
− ("rona", "RONA", "RONA", "rona"), |
| 48 |
|
−] |
| 49 |
|
− |
| 50 |
|
−_BAN = ("station", "stationnement") |
|
40 |
+# id -> (libellé, requête Mapbox, mot-clé de validation — le nom du POI doit le |
|
41 |
+# contenir, sans accents ni casse ; écarte « Station Métro », « Super Qualité »…) |
|
42 |
+BRAND_DEFS = { |
|
43 |
+ # nationaux |
|
44 |
+ "costco": ("Costco", "Costco Wholesale", "costco"), |
|
45 |
+ "walmart": ("Walmart", "Walmart Supercentre", "walmart"), |
|
46 |
+ "canadiantire": ("Canadian Tire", "Canadian Tire", "canadian tire"), |
|
47 |
+ "dollarama": ("Dollarama", "Dollarama", "dollarama"), |
|
48 |
+ "homedepot": ("Home Depot", "Home Depot", "home depot"), |
|
49 |
+ "homehardware": ("Home Hardware", "Home Hardware", "home hardware"), |
|
50 |
+ "shoppers": ("Shoppers Drug Mart", "Shoppers Drug Mart", "shoppers"), |
|
51 |
+ "gianttiger": ("Giant Tiger", "Giant Tiger", "giant tiger"), |
|
52 |
+ # épicerie — Loblaw |
|
53 |
+ "loblaws": ("Loblaws", "Loblaws", "loblaws"), |
|
54 |
+ "superstore": ("Real Canadian Superstore", "Real Canadian Superstore", "superstore"), |
|
55 |
+ "atlanticsuperstore": ("Atlantic Superstore", "Atlantic Superstore", "superstore"), |
|
56 |
+ "nofrills": ("No Frills", "No Frills", "no frills"), |
|
57 |
+ "zehrs": ("Zehrs", "Zehrs", "zehrs"), |
|
58 |
+ "dominion": ("Dominion", "Dominion grocery", "dominion"), |
|
59 |
+ "independent": ("Your Independent Grocer", "Your Independent Grocer", "independent"), |
|
60 |
+ # épicerie — Empire/Sobeys |
|
61 |
+ "sobeys": ("Sobeys", "Sobeys", "sobeys"), |
|
62 |
+ "safeway": ("Safeway", "Safeway", "safeway"), |
|
63 |
+ "foodland": ("Foodland", "Foodland", "foodland"), |
|
64 |
+ "freshco": ("FreshCo", "FreshCo", "freshco"), |
|
65 |
+ # épicerie — Metro (Ontario) |
|
66 |
+ "metro": ("Metro", "Metro grocery", "metro"), |
|
67 |
+ "foodbasics": ("Food Basics", "Food Basics", "food basics"), |
|
68 |
+ # épicerie — Ouest / coopératives / régionales |
|
69 |
+ "saveon": ("Save-On-Foods", "Save-On-Foods", "save-on"), |
|
70 |
+ "coop": ("Co-op", "Co-op Food Store", "co-op"), |
|
71 |
+ "colemans": ("Colemans", "Colemans grocery", "colemans"), |
|
72 |
+ "iga": ("IGA", "IGA", "iga"), |
|
73 |
+ # alcool (monopoles/sociétés provinciales) |
|
74 |
+ "lcbo": ("LCBO", "LCBO", "lcbo"), |
|
75 |
+ "beerstore": ("The Beer Store", "The Beer Store", "beer store"), |
|
76 |
+ "bcliquor": ("BC Liquor", "BC Liquor Store", "liquor"), |
|
77 |
+ "liquormart": ("Liquor Mart", "Manitoba Liquor Mart", "liquor mart"), |
|
78 |
+ "nbliquor": ("NB Liquor", "NB Liquor Alcool NB", "liquor"), |
|
79 |
+ "nslc": ("NSLC", "NSLC", "nslc"), |
|
80 |
+ # pharmacies régionales |
|
81 |
+ "londondrugs": ("London Drugs", "London Drugs", "london drugs"), |
|
82 |
+ "rexall": ("Rexall", "Rexall", "rexall"), |
|
83 |
+ "lawtons": ("Lawtons", "Lawtons Drugs", "lawtons"), |
|
84 |
+ # quincaillerie |
|
85 |
+ "rona": ("RONA", "RONA", "rona"), |
|
86 |
+ "kent": ("Kent", "Kent Building Supplies", "kent"), |
|
87 |
+} |
|
88 |
+ |
|
89 |
+_NATIONAL = ["costco", "walmart", "canadiantire", "dollarama", "homedepot", |
|
90 |
+ "shoppers"] |
|
91 |
+ |
|
92 |
+# panier par province — les bannières LES PLUS PRÉSENTES dans chaque marché |
|
93 |
+PROVINCE_BRANDS = { |
|
94 |
+ "British Columbia": _NATIONAL + ["saveon", "superstore", "safeway", |
|
95 |
+ "nofrills", "iga", "londondrugs", |
|
96 |
+ "bcliquor", "homehardware"], |
|
97 |
+ "Alberta": _NATIONAL + ["superstore", "safeway", "saveon", "sobeys", |
|
98 |
+ "nofrills", "coop", "rexall", "homehardware"], |
|
99 |
+ "Saskatchewan": _NATIONAL + ["coop", "superstore", "sobeys", "safeway", |
|
100 |
+ "nofrills", "gianttiger", "homehardware"], |
|
101 |
+ "Manitoba": _NATIONAL + ["superstore", "coop", "sobeys", "safeway", |
|
102 |
+ "nofrills", "liquormart", "gianttiger", |
|
103 |
+ "homehardware"], |
|
104 |
+ "Ontario": _NATIONAL + ["loblaws", "nofrills", "foodbasics", "metro", |
|
105 |
+ "sobeys", "freshco", "zehrs", "lcbo", "beerstore", |
|
106 |
+ "rexall", "rona"], |
|
107 |
+ "New Brunswick": _NATIONAL + ["sobeys", "atlanticsuperstore", "foodland", |
|
108 |
+ "nofrills", "nbliquor", "kent", "lawtons", |
|
109 |
+ "gianttiger"], |
|
110 |
+ "Nova Scotia": _NATIONAL + ["sobeys", "atlanticsuperstore", "foodland", |
|
111 |
+ "nofrills", "nslc", "kent", "lawtons", |
|
112 |
+ "gianttiger"], |
|
113 |
+ "Prince Edward Island": _NATIONAL + ["sobeys", "atlanticsuperstore", |
|
114 |
+ "foodland", "kent", "lawtons", |
|
115 |
+ "gianttiger"], |
|
116 |
+ "Newfoundland and Labrador": _NATIONAL + ["sobeys", "dominion", "colemans", |
|
117 |
+ "foodland", "kent", "lawtons"], |
|
118 |
+ "Yukon": _NATIONAL + ["saveon", "superstore", "independent", |
|
119 |
+ "homehardware"], |
|
120 |
+ "Northwest Territories": _NATIONAL + ["independent", "coop", |
|
121 |
+ "homehardware"], |
|
122 |
+ "Nunavut": ["canadiantire", "independent", "coop", "homehardware"], |
|
123 |
+} |
|
124 |
+_DEFAULT_PROVINCE = "Ontario" |
|
125 |
+ |
|
126 |
+# frontières longitudinales approximatives (repli quand la région est absente) |
|
127 |
+def _infer_province(lat: float, lng: float) -> str: |
|
128 |
+ if lat >= 60: |
|
129 |
+ if lng < -124: |
|
130 |
+ return "Yukon" |
|
131 |
+ return "Northwest Territories" if lng < -102 else "Nunavut" |
|
132 |
+ if lng < -120: |
|
133 |
+ return "British Columbia" |
|
134 |
+ if lng < -110: |
|
135 |
+ return "Alberta" |
|
136 |
+ if lng < -101.4: |
|
137 |
+ return "Saskatchewan" |
|
138 |
+ if lng < -95.15: |
|
139 |
+ return "Manitoba" |
|
140 |
+ if lng < -74.3: |
|
141 |
+ return "Ontario" |
|
142 |
+ if -64.5 <= lng <= -61.9 and 45.9 <= lat <= 47.1: |
|
143 |
+ return "Prince Edward Island" |
|
144 |
+ if lng >= -59.5 or lat >= 50.5: |
|
145 |
+ return "Newfoundland and Labrador" |
|
146 |
+ if lat < 46.05 or lng > -64.4: |
|
147 |
+ return "Nova Scotia" |
|
148 |
+ return "New Brunswick" |
|
149 |
+ |
|
150 |
+ |
|
151 |
+def _brands_for(region: str | None, lat: float, lng: float): |
|
152 |
+ prov = (region or "").strip() |
|
153 |
+ if prov not in PROVINCE_BRANDS: |
|
154 |
+ prov = _infer_province(lat, lng) |
|
155 |
+ ids = PROVINCE_BRANDS.get(prov, PROVINCE_BRANDS[_DEFAULT_PROVINCE]) |
|
156 |
+ return [(bid, *BRAND_DEFS[bid]) for bid in ids if bid in BRAND_DEFS] |
|
157 |
+ |
|
158 |
+_BAN = ("station", "stationnement", "kentucky", "kentville", |
|
159 |
+ "co-operators", "cooperators") |
| 51 |
160 |
|
| 52 |
161 |
|
| 53 |
162 |
def _norm(s: str) -> str: |
| 227 |
336 |
return out |
| 228 |
337 |
|
| 229 |
338 |
|
| 230 |
|
−def nearby(lat: float, lng: float) -> dict: |
| 231 |
|
− """Grand commerce le plus proche par bannière (cache ~1 km, TTL 30 j).""" |
|
339 |
+def nearby(lat: float, lng: float, region: str | None = None) -> dict: |
|
340 |
+ """Grand commerce le plus proche par bannière PROVINCIALE (cache ~1 km).""" |
|
341 |
+ brands = _brands_for(region, lat, lng) |
| 232 |
342 |
cell = f"{round(lat, 2)},{round(lng, 2)}" |
| 233 |
343 |
con = _connect() |
| 234 |
344 |
now = time.time() |
| 235 |
345 |
cached = {r["brand"]: r for r in con.execute( |
| 236 |
346 |
"SELECT * FROM commerces_cache WHERE cellule=? AND fetched_at>?", |
| 237 |
347 |
(cell, now - TTL))} |
| 238 |
|
− manquants = [(bid, q, m) for bid, _, q, m in BRANDS |
|
348 |
+ manquants = [(bid, q, m) for bid, _, q, m in brands |
| 239 |
349 |
if bid not in cached] |
| 240 |
350 |
transit_manquant = any(k not in cached for k, _ in TRANSIT) |
| 241 |
351 |
if manquants or transit_manquant: |
| 279 |
389 |
"nom": r["nom"], "adresse": "", |
| 280 |
390 |
"dist_m": round(d), |
| 281 |
391 |
"lat": r["lat"], "lng": r["lng"]}) |
| 282 |
|
− for bid, label, _q, _m in BRANDS: |
|
392 |
+ for bid, label, _q, _m in brands: |
| 283 |
393 |
r = cached.get(bid) |
| 284 |
394 |
if r is None: |
| 285 |
395 |
continue |