SPB Git

spb/house-ka

Public

feat(commerces): bannieres de proximite adaptees a chaque province

Paniers par province fondes sur la presence reelle des chaines (Loblaw n.1
national, Sobeys n.2 dominant Atlantique, Metro Ontario, Pattison/Save-On-Foods
~180 magasins Ouest+Yukon, Co-op ~300 magasins SK, monopoles d alcool
provinciaux LCBO/BC Liquor/Liquor Mart/NB Liquor/NSLC, Kent Irving Atlantique,
Dominion et Colemans a Terre-Neuve, London Drugs BC/AB).

- commerces.py : BRAND_DEFS + PROVINCE_BRANDS + inference lat/lng de repli
- /api/commerces?region=<province> ; fiche passe l.region
- NearbyPlaces : ~30 pastilles monogrammes aux couleurs des bannieres
Simon-Pierre Boucher committed (Aug 27, 2026) parent 852c914 Browse files

5 changed files +196 −51

modified frontend/src/api.ts +4 −2
@@ -379,5 +379,7 @@ export interface CommercesNearby {
379 379 n: number; commerces: CommerceItem[]; transit?: CommerceItem[];
380 380 }
381 381
382 −export const fetchCommerces = (lat: number, lng: number) =>
383 − get<CommercesNearby>(`/api/commerces?lat=${lat}&lng=${lng}`);
382 +export const fetchCommerces = (lat: number, lng: number, region?: string) =>
383 + get<CommercesNearby>(
384 + `/api/commerces?lat=${lat}&lng=${lng}` +
385 + (region ? `&region=${encodeURIComponent(region)}` : ""));
modified frontend/src/components/NearbyPlaces.tsx +42 −12
@@ -15,20 +15,50 @@ const ICONS: Record<string, [string, string, string?]> = {
15 15 rem_station: ["#84BD00", "R"],
16 16 arret_bus: ["#4E5357", "B"],
17 17 gare_train: ["#6E5B3F", "T"],
18 + // national
18 19 costco: ["#005DAA", "C"],
19 20 walmart: ["#0071CE", "W"],
20 − metro: ["#EF3E42", "M"],
21 − iga: ["#D50032", "IGA"],
22 − maxi: ["#0079C1", "Mx"],
23 − superc: ["#E4002B", "SC"],
24 − provigo: ["#DA291C", "P"],
25 21 canadiantire: ["#D6001C", "CT"],
26 22 dollarama: ["#00B140", "D", "#FFDD00"],
27 − saq: ["#892034", "SAQ"],
28 − pharmaprix: ["#E11B22", "Ph"],
29 − jeancoutu: ["#003DA5", "JC"],
30 23 homedepot: ["#F96302", "HD"],
24 + homehardware: ["#DA291C", "HH"],
25 + shoppers: ["#E51937", "SDM"],
26 + gianttiger: ["#FFD200", "GT", "#D6001C"],
27 + // grocery — Loblaw banners
28 + loblaws: ["#E4002B", "L"],
29 + superstore: ["#D22630", "RCS"],
30 + atlanticsuperstore: ["#D22630", "AS"],
31 + nofrills: ["#FFDD00", "NF", "#111111"],
32 + zehrs: ["#C8102E", "Z"],
33 + dominion: ["#C8102E", "Do"],
34 + independent: ["#C8102E", "YIG"],
35 + // grocery — Empire/Sobeys banners
36 + sobeys: ["#00563F", "S"],
37 + safeway: ["#ED1C24", "SW"],
38 + foodland: ["#00794A", "FL"],
39 + freshco: ["#54B948", "FC"],
40 + // grocery — Metro (Ontario)
41 + metro: ["#EF3E42", "M"],
42 + foodbasics: ["#EE3124", "FB"],
43 + // grocery — western / co-ops / regional
44 + saveon: ["#00543D", "SOF"],
45 + coop: ["#00843D", "CO"],
46 + colemans: ["#0067B1", "Cm"],
47 + iga: ["#D50032", "IGA"],
48 + // provincial liquor boards
49 + lcbo: ["#00532A", "LC"],
50 + beerstore: ["#1E4620", "TBS"],
51 + bcliquor: ["#003E51", "BCL"],
52 + liquormart: ["#004F9F", "LM"],
53 + nbliquor: ["#00539F", "NBL"],
54 + nslc: ["#0B4F32", "NSL"],
55 + // regional pharmacies
56 + londondrugs: ["#004F9F", "LD"],
57 + rexall: ["#003087", "Rx"],
58 + lawtons: ["#00539F", "Lw"],
59 + // hardware
31 60 rona: ["#1B4298", "R"],
61 + kent: ["#006A4D", "K"],
32 62 };
33 63
34 64 function Chip({ id }: { id: string }) {
@@ -47,14 +77,14 @@ function Chip({ id }: { id: string }) {
47 77 );
48 78 }
49 79
50 −export default function NearbyPlaces({ lat, lng }:
51 − { lat: number | null; lng: number | null }) {
80 +export default function NearbyPlaces({ lat, lng, region }:
81 + { lat: number | null; lng: number | null; region?: string }) {
52 82 const [d, setD] = useState<CommercesNearby | null>(null);
53 83 useEffect(() => {
54 84 setD(null);
55 85 if (lat == null || lng == null) return;
56 − fetchCommerces(lat, lng).then(setD).catch(() => setD(null));
57 − }, [lat, lng]);
86 + fetchCommerces(lat, lng, region).then(setD).catch(() => setD(null));
87 + }, [lat, lng, region]);
58 88 if (lat == null || lng == null || !d) return null;
59 89 const all = [...(d.transit ?? []), ...(d.commerces ?? [])];
60 90 if (all.length === 0) return null;
modified frontend/src/pages/Listing.tsx +4 −2
@@ -425,8 +425,10 @@ export default function ListingPage() {
425 425 {/* financing: real mortgage rates + Canadian calculator */}
426 426 {isSale && <Financing price={l.price} />}
427 427
428 − {/* nearby amenities: full width, AFTER the property info (correct mobile order) */}
429 − <NearbyPlaces lat={l.lat} lng={l.lng} />
428 + {/* nearby amenities: full width, AFTER the property info (correct mobile order) —
429 + the banner basket is per-province (Save-On-Foods in BC, Sobeys in the
430 + Atlantic, Co-op in SK, LCBO in ON…) */}
431 + <NearbyPlaces lat={l.lat} lng={l.lng} region={l.region} />
430 432
431 433 <div className="fine f-foot">
432 434 Prices and availability are those displayed by the source — every
modified immoka/commerces.py +142 −32
@@ -1,14 +1,23 @@
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,26 +37,126 @@ UA = "HouseKaBot/1.0 (+https://www.house-ka.com; contact@spboucher.ai)"
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,15 +336,16 @@ def _transit_from_poi(lat: float, lng: float) -> list[tuple[str, dict]]:
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,7 +389,7 @@ def nearby(lat: float, lng: float) -> dict:
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
modified immoka/web.py +4 −3
@@ -175,10 +175,11 @@ def list_listings(
175 175
176 176
177 177 @app.get("/api/commerces")
178 −def commerces_at(lat: float, lng: float):
179 − """Grands commerces + métro/bus les plus proches (Mapbox / OSM)."""
178 +def commerces_at(lat: float, lng: float, region: str | None = None):
179 + """Grands commerces (bannières PAR PROVINCE) + transit les plus proches
180 + (Mapbox / OSM). `region` = province de l'annonce ; sinon inférée du point."""
180 181 from . import commerces
181 − return commerces.nearby(lat, lng)
182 + return commerces.nearby(lat, lng, region=region)
182 183
183 184
184 185 @app.get("/api/listings/{uid}")
185 186
Menu

Theme