| 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 | # Auteur : Simon-Pierre Boucher — contact@spboucher.ai |
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 | +# Pour chaque grande bannière du panier provincial, on interroge l'API |
| 7 | −# l'API Search Box de Mapbox (jeton PUBLIC pk.… lu dans |
8 | +# Search Box de Mapbox (jeton PUBLIC pk.… lu dans |
| 8 | −# frontend/src/kamaps/config.ts — source de vérité du projet) avec la |
9 | +# frontend/src/kamaps/config.ts) avec la position de l'annonce en |
| 9 | −# position de l'annonce en `proximity`, et on retient le point de vente le |
10 | +# `proximity`, et on retient le point de vente le plus proche. Cache par |
| 10 | −# plus proche. Cache par cellule d'environ 1 km (data/commerces.db, |
11 | +# cellule d'environ 1 km (data/commerces.db, TTL 30 jours). |
| 11 | −# TTL 30 jours) : les fiches d'un même secteur ne recoûtent rien. |
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 | from __future__ import annotations |
22 | from __future__ import annotations |
| 14 | |
23 | |
| 28 | TTL = 30 * 86400 |
37 | TTL = 30 * 86400 |
| 29 | API = "https://api.mapbox.com/search/searchbox/v1/forward" |
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 |
40 | +# 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é »…) |
41 | +# contenir, sans accents ni casse ; écarte « Station Métro », « Super Qualité »…) |
| 33 | −BRANDS = [ |
42 | +BRAND_DEFS = { |
| 34 | − ("costco", "Costco", "Costco Wholesale", "costco"), |
43 | + # nationaux |
| 35 | − ("walmart", "Walmart", "Walmart Supercentre", "walmart"), |
44 | + "costco": ("Costco", "Costco Wholesale", "costco"), |
| 36 | − ("metro", "Metro", "Metro", "metro"), |
45 | + "walmart": ("Walmart", "Walmart Supercentre", "walmart"), |
| 37 | − ("iga", "IGA", "IGA", "iga"), |
46 | + "canadiantire": ("Canadian Tire", "Canadian Tire", "canadian tire"), |
| 38 | − ("maxi", "Maxi", "Maxi", "maxi"), |
47 | + "dollarama": ("Dollarama", "Dollarama", "dollarama"), |
| 39 | − ("superc", "Super C", "Super C", "super c"), |
48 | + "homedepot": ("Home Depot", "Home Depot", "home depot"), |
| 40 | − ("provigo", "Provigo", "Provigo", "provigo"), |
49 | + "homehardware": ("Home Hardware", "Home Hardware", "home hardware"), |
| 41 | − ("canadiantire", "Canadian Tire", "Canadian Tire", "canadian tire"), |
50 | + "shoppers": ("Shoppers Drug Mart", "Shoppers Drug Mart", "shoppers"), |
| 42 | − ("dollarama", "Dollarama", "Dollarama", "dollarama"), |
51 | + "gianttiger": ("Giant Tiger", "Giant Tiger", "giant tiger"), |
| 43 | − ("saq", "SAQ", "SAQ", "saq"), |
52 | + # épicerie — Loblaw |
| 44 | − ("pharmaprix", "Pharmaprix", "Pharmaprix", "pharmaprix"), |
53 | + "loblaws": ("Loblaws", "Loblaws", "loblaws"), |
| 45 | − ("jeancoutu", "Jean Coutu", "Jean Coutu pharmacie", "jean coutu"), |
54 | + "superstore": ("Real Canadian Superstore", "Real Canadian Superstore", "superstore"), |
| 46 | − ("homedepot", "Home Depot", "Home Depot", "home depot"), |
55 | + "atlanticsuperstore": ("Atlantic Superstore", "Atlantic Superstore", "superstore"), |
| 47 | − ("rona", "RONA", "RONA", "rona"), |
56 | + "nofrills": ("No Frills", "No Frills", "no frills"), |
| 48 | −] |
57 | + "zehrs": ("Zehrs", "Zehrs", "zehrs"), |
| 49 | − |
58 | + "dominion": ("Dominion", "Dominion grocery", "dominion"), |
| 50 | −_BAN = ("station", "stationnement") |
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 | def _norm(s: str) -> str: |
162 | def _norm(s: str) -> str: |
| 227 | return out |
336 | return out |
| 228 | |
337 | |
| 229 | |
338 | |
| 230 | −def nearby(lat: float, lng: float) -> dict: |
339 | +def nearby(lat: float, lng: float, region: str | None = None) -> dict: |
| 231 | − """Grand commerce le plus proche par bannière (cache ~1 km, TTL 30 j).""" |
340 | + """Grand commerce le plus proche par bannière PROVINCIALE (cache ~1 km).""" |
| |
341 | + brands = _brands_for(region, lat, lng) |
| 232 | cell = f"{round(lat, 2)},{round(lng, 2)}" |
342 | cell = f"{round(lat, 2)},{round(lng, 2)}" |
| 233 | con = _connect() |
343 | con = _connect() |
| 234 | now = time.time() |
344 | now = time.time() |
| 235 | cached = {r["brand"]: r for r in con.execute( |
345 | cached = {r["brand"]: r for r in con.execute( |
| 236 | "SELECT * FROM commerces_cache WHERE cellule=? AND fetched_at>?", |
346 | "SELECT * FROM commerces_cache WHERE cellule=? AND fetched_at>?", |
| 237 | (cell, now - TTL))} |
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 | if bid not in cached] |
349 | if bid not in cached] |
| 240 | transit_manquant = any(k not in cached for k, _ in TRANSIT) |
350 | transit_manquant = any(k not in cached for k, _ in TRANSIT) |
| 241 | if manquants or transit_manquant: |
351 | if manquants or transit_manquant: |
| 279 | "nom": r["nom"], "adresse": "", |
389 | "nom": r["nom"], "adresse": "", |
| 280 | "dist_m": round(d), |
390 | "dist_m": round(d), |
| 281 | "lat": r["lat"], "lng": r["lng"]}) |
391 | "lat": r["lat"], "lng": r["lng"]}) |
| 282 | − for bid, label, _q, _m in BRANDS: |
392 | + for bid, label, _q, _m in brands: |
| 283 | r = cached.get(bid) |
393 | r = cached.get(bid) |
| 284 | if r is None: |
394 | if r is None: |
| 285 | continue |
395 | continue |