feat(ontario v1): connecteur générique RealtyPress — 13 sites d'agences ON (CREA DDF)
- connectors/realtypress.py : archive /listing/page/N/?posts_per_page=100, cookie disclaimer=accepted (mur CREA), 2 variantes de thème (ville avec/sans <strong>, ft²), fiches DDF (MLS®, tableaux, GPS JSON-LD, photos ddfcdn), external_id ddf<id> + infixe _ag_ → dédup inter-sites par external_id - data/ontario_agencies.json : registre 13 sites (Ottawa, KW, Kingston, Grey-Bruce, Collingwood, PEC, Pembroke, Huron-Perth, Thunder Bay, Sault, North Bay) ; sources.json : 13 entrées rp_ag_* - pipeline Ontario-aware : schema.finalize bbox 41.6–63/-95.5–-56, normalize._TYPE_MAP mots-clés anglais (detached, semi-detached, row house, multi-family, mobile home, farm, retail…), geocode _BBOX_KA + fallback Nominatim « city, Ontario, Canada » + run_batch exclut region='Ontario' (Adresses Québec ne couvre pas l'ON ; le DDF fournit lat/lng) Testé : cbnorthbay ingéré (503 fiches, 150 enrichies publiées score 94, stubs en quarantaine jusqu'à enrichissement), labrosse/sutton spot-checkés, API /api/listings?region=Ontario OK, parc QC intact (94 528 publiées). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6 changed files +569 −40
added
data/ontario_agencies.json
+80 −0
@@ -0,0 +1,80 @@ | ||
| 1 | +[ | |
| 2 | + { | |
| 3 | + "id": "rp_ag_suttonottawa", | |
| 4 | + "name": "Sutton Group — Ottawa Realty", | |
| 5 | + "site": "https://suttonottawa.ca", | |
| 6 | + "note": "Recensement ON 2026-08-27 : ~10 080 fiches (OREB+). RealtyPress/DDF." | |
| 7 | + }, | |
| 8 | + { | |
| 9 | + "id": "rp_ag_helensteam", | |
| 10 | + "name": "Helen's Team (Kitchener-Waterloo)", | |
| 11 | + "site": "https://helensteam.ca", | |
| 12 | + "note": "Recensement ON 2026-08-27 : ~9 910 fiches (Kitchener-Waterloo). RealtyPress/DDF." | |
| 13 | + }, | |
| 14 | + { | |
| 15 | + "id": "rp_ag_greybruce", | |
| 16 | + "name": "Grey Bruce Real Estate", | |
| 17 | + "site": "https://greybrucerealestate.ca", | |
| 18 | + "note": "Recensement ON 2026-08-27 : ~8 268 fiches (Grey-Bruce/Georgian Bay). RealtyPress/DDF. Même feed que collaborativerealestate.ca (fallback)." | |
| 19 | + }, | |
| 20 | + { | |
| 21 | + "id": "rp_ag_remaxfinest", | |
| 22 | + "name": "RE/MAX Finest Realty (Kingston)", | |
| 23 | + "site": "https://remaxfinestrealty.com", | |
| 24 | + "note": "Recensement ON 2026-08-27 : ~8 217 fiches (Kingston). RealtyPress/DDF." | |
| 25 | + }, | |
| 26 | + { | |
| 27 | + "id": "rp_ag_riouxbaker", | |
| 28 | + "name": "Rioux Baker Real Estate Team (Collingwood)", | |
| 29 | + "site": "https://riouxbakerteam.com", | |
| 30 | + "note": "Recensement ON 2026-08-27 : ~7 716 fiches (Collingwood/South Georgian Bay). RealtyPress/DDF." | |
| 31 | + }, | |
| 32 | + { | |
| 33 | + "id": "rp_ag_countyguys", | |
| 34 | + "name": "The County Guys (Prince Edward County)", | |
| 35 | + "site": "https://thecountyguys.com", | |
| 36 | + "note": "Recensement ON 2026-08-27 : ~6 878 fiches (Prince Edward County/Quinte). RealtyPress/DDF." | |
| 37 | + }, | |
| 38 | + { | |
| 39 | + "id": "rp_ag_labrosse", | |
| 40 | + "name": "Labrosse Real Estate (Ottawa/Orléans)", | |
| 41 | + "site": "https://labrosserealestate.com", | |
| 42 | + "note": "Recensement ON 2026-08-27 : ~6 873 fiches (Ottawa/Orléans, équipe FRANCOPHONE). RealtyPress/DDF." | |
| 43 | + }, | |
| 44 | + { | |
| 45 | + "id": "rp_ag_ryanpattinson", | |
| 46 | + "name": "Ryan Pattinson (Pembroke/Renfrew)", | |
| 47 | + "site": "https://ryanpattinson.com", | |
| 48 | + "note": "Recensement ON 2026-08-27 : ~6 601 fiches (Pembroke/vallée de l'Outaouais ON). RealtyPress/DDF." | |
| 49 | + }, | |
| 50 | + { | |
| 51 | + "id": "rp_ag_grapevine", | |
| 52 | + "name": "Grapevine (Ottawa)", | |
| 53 | + "site": "https://grapevine.ca", | |
| 54 | + "note": "Recensement ON 2026-08-27 : ~6 550 fiches (Ottawa). RealtyPress/DDF. Suivre les redirections www/apex." | |
| 55 | + }, | |
| 56 | + { | |
| 57 | + "id": "rp_ag_rlpheartland", | |
| 58 | + "name": "Royal LePage Heartland Realty", | |
| 59 | + "site": "https://rlpheartland.ca", | |
| 60 | + "note": "Recensement ON 2026-08-27 : ~5 577 fiches (Midwestern Ontario — Huron/Perth). RealtyPress/DDF." | |
| 61 | + }, | |
| 62 | + { | |
| 63 | + "id": "rp_ag_signaturenorth", | |
| 64 | + "name": "Signature North Realty (Thunder Bay)", | |
| 65 | + "site": "https://signaturenorthrealty.ca", | |
| 66 | + "note": "Recensement ON 2026-08-27 : ~1 023 fiches (Thunder Bay). RealtyPress/DDF." | |
| 67 | + }, | |
| 68 | + { | |
| 69 | + "id": "rp_ag_saultstemarie", | |
| 70 | + "name": "Sault Ste. Marie Real Estate (Century 21 Choice)", | |
| 71 | + "site": "https://saultstemarierealestate.com", | |
| 72 | + "note": "Recensement ON 2026-08-27 : ~864 fiches (Sault Ste. Marie). RealtyPress/DDF." | |
| 73 | + }, | |
| 74 | + { | |
| 75 | + "id": "rp_ag_cbnorthbay", | |
| 76 | + "name": "Coldwell Banker Peter Minogue (North Bay)", | |
| 77 | + "site": "https://cbnorthbay.com", | |
| 78 | + "note": "Recensement ON 2026-08-27 : ~307 fiches (North Bay). RealtyPress/DDF." | |
| 79 | + } | |
| 80 | +] | |
modified
data/sources.json
+144 −1
@@ -1,5 +1,5 @@ | ||
| 1 | 1 | { |
| 2 | − "_comment": "Immo-Ka — Registre des sources (agences de courtage immobilier, province de Québec). Un connecteur par agence. Auteur : Simon-Pierre Boucher — contact@spboucher.ai", | |
| 2 | + "_comment": "Immo-Ka — Registre des sources (agences de courtage immobilier, Québec + Ontario). Un connecteur par agence. Auteur : Simon-Pierre Boucher — contact@spboucher.ai", | |
| 3 | 3 | "sources": [ |
| 4 | 4 | { |
| 5 | 5 | "id": "remax_quebec", |
@@ -867,6 +867,149 @@ | ||
| 867 | 867 | "status": "actif", |
| 868 | 868 | "type": "agence", |
| 869 | 869 | "note": "Site web « classique » server-rendered, connecteur générique agences_web.py (registre data/web_agencies.json). Découverte via recensement OACIQ 2026-08-24. Fiches Next.js rendues via Scrapfly. OACIQ 2026-08 : 9 courtiers (Montréal). Next.js : fiches rendues via Scrapfly." |
| 870 | + }, | |
| 871 | + { | |
| 872 | + "id": "rp_ag_suttonottawa", | |
| 873 | + "name": "Sutton Group — Ottawa Realty", | |
| 874 | + "url": "https://suttonottawa.ca", | |
| 875 | + "listing_url": "https://suttonottawa.ca/listing/", | |
| 876 | + "coverage": "Ottawa et l'Est ontarien (board OREB) — ~10 000 fiches DDF", | |
| 877 | + "connector": "realtypress", | |
| 878 | + "status": "actif", | |
| 879 | + "type": "agence", | |
| 880 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~10 080 fiches (OREB+). RealtyPress/DDF." | |
| 881 | + }, | |
| 882 | + { | |
| 883 | + "id": "rp_ag_helensteam", | |
| 884 | + "name": "Helen's Team (Kitchener-Waterloo)", | |
| 885 | + "url": "https://helensteam.ca", | |
| 886 | + "listing_url": "https://helensteam.ca/listing/", | |
| 887 | + "coverage": "Kitchener-Waterloo et région (ITSO) — ~9 900 fiches DDF", | |
| 888 | + "connector": "realtypress", | |
| 889 | + "status": "actif", | |
| 890 | + "type": "agence", | |
| 891 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~9 910 fiches (Kitchener-Waterloo). RealtyPress/DDF." | |
| 892 | + }, | |
| 893 | + { | |
| 894 | + "id": "rp_ag_greybruce", | |
| 895 | + "name": "Grey Bruce Real Estate", | |
| 896 | + "url": "https://greybrucerealestate.ca", | |
| 897 | + "listing_url": "https://greybrucerealestate.ca/listing/", | |
| 898 | + "coverage": "Grey-Bruce / Georgian Bay (ITSO) — ~8 300 fiches DDF", | |
| 899 | + "connector": "realtypress", | |
| 900 | + "status": "actif", | |
| 901 | + "type": "agence", | |
| 902 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~8 268 fiches (Grey-Bruce/Georgian Bay). RealtyPress/DDF. Même feed que collaborativerealestate.ca (fallback)." | |
| 903 | + }, | |
| 904 | + { | |
| 905 | + "id": "rp_ag_remaxfinest", | |
| 906 | + "name": "RE/MAX Finest Realty (Kingston)", | |
| 907 | + "url": "https://remaxfinestrealty.com", | |
| 908 | + "listing_url": "https://remaxfinestrealty.com/listing/", | |
| 909 | + "coverage": "Kingston et région (KAREA) — ~8 200 fiches DDF", | |
| 910 | + "connector": "realtypress", | |
| 911 | + "status": "actif", | |
| 912 | + "type": "agence", | |
| 913 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~8 217 fiches (Kingston). RealtyPress/DDF." | |
| 914 | + }, | |
| 915 | + { | |
| 916 | + "id": "rp_ag_riouxbaker", | |
| 917 | + "name": "Rioux Baker Real Estate Team (Collingwood)", | |
| 918 | + "url": "https://riouxbakerteam.com", | |
| 919 | + "listing_url": "https://riouxbakerteam.com/listing/", | |
| 920 | + "coverage": "Collingwood / South Georgian Bay — ~7 700 fiches DDF", | |
| 921 | + "connector": "realtypress", | |
| 922 | + "status": "actif", | |
| 923 | + "type": "agence", | |
| 924 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~7 716 fiches (Collingwood/South Georgian Bay). RealtyPress/DDF." | |
| 925 | + }, | |
| 926 | + { | |
| 927 | + "id": "rp_ag_countyguys", | |
| 928 | + "name": "The County Guys (Prince Edward County)", | |
| 929 | + "url": "https://thecountyguys.com", | |
| 930 | + "listing_url": "https://thecountyguys.com/listing/", | |
| 931 | + "coverage": "Prince Edward County / Quinte — ~6 900 fiches DDF", | |
| 932 | + "connector": "realtypress", | |
| 933 | + "status": "actif", | |
| 934 | + "type": "agence", | |
| 935 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~6 878 fiches (Prince Edward County/Quinte). RealtyPress/DDF." | |
| 936 | + }, | |
| 937 | + { | |
| 938 | + "id": "rp_ag_labrosse", | |
| 939 | + "name": "Labrosse Real Estate (Ottawa/Orléans)", | |
| 940 | + "url": "https://labrosserealestate.com", | |
| 941 | + "listing_url": "https://labrosserealestate.com/listing/", | |
| 942 | + "coverage": "Ottawa / Orléans (équipe francophone, OREB) — ~6 900 fiches DDF", | |
| 943 | + "connector": "realtypress", | |
| 944 | + "status": "actif", | |
| 945 | + "type": "agence", | |
| 946 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~6 873 fiches (Ottawa/Orléans, équipe FRANCOPHONE). RealtyPress/DDF." | |
| 947 | + }, | |
| 948 | + { | |
| 949 | + "id": "rp_ag_ryanpattinson", | |
| 950 | + "name": "Ryan Pattinson (Pembroke/Renfrew)", | |
| 951 | + "url": "https://ryanpattinson.com", | |
| 952 | + "listing_url": "https://ryanpattinson.com/listing/", | |
| 953 | + "coverage": "Pembroke / vallée de l'Outaouais ontarienne — ~6 600 fiches DDF", | |
| 954 | + "connector": "realtypress", | |
| 955 | + "status": "actif", | |
| 956 | + "type": "agence", | |
| 957 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~6 601 fiches (Pembroke/vallée de l'Outaouais ON). RealtyPress/DDF." | |
| 958 | + }, | |
| 959 | + { | |
| 960 | + "id": "rp_ag_grapevine", | |
| 961 | + "name": "Grapevine (Ottawa)", | |
| 962 | + "url": "https://grapevine.ca", | |
| 963 | + "listing_url": "https://grapevine.ca/listing/", | |
| 964 | + "coverage": "Ottawa (OREB) — ~6 600 fiches DDF", | |
| 965 | + "connector": "realtypress", | |
| 966 | + "status": "actif", | |
| 967 | + "type": "agence", | |
| 968 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~6 550 fiches (Ottawa). RealtyPress/DDF. Suivre les redirections www/apex." | |
| 969 | + }, | |
| 970 | + { | |
| 971 | + "id": "rp_ag_rlpheartland", | |
| 972 | + "name": "Royal LePage Heartland Realty", | |
| 973 | + "url": "https://rlpheartland.ca", | |
| 974 | + "listing_url": "https://rlpheartland.ca/listing/", | |
| 975 | + "coverage": "Midwestern Ontario (Huron-Perth) — ~5 600 fiches DDF", | |
| 976 | + "connector": "realtypress", | |
| 977 | + "status": "actif", | |
| 978 | + "type": "agence", | |
| 979 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~5 577 fiches (Midwestern Ontario — Huron/Perth). RealtyPress/DDF." | |
| 980 | + }, | |
| 981 | + { | |
| 982 | + "id": "rp_ag_signaturenorth", | |
| 983 | + "name": "Signature North Realty (Thunder Bay)", | |
| 984 | + "url": "https://signaturenorthrealty.ca", | |
| 985 | + "listing_url": "https://signaturenorthrealty.ca/listing/", | |
| 986 | + "coverage": "Thunder Bay — ~1 000 fiches DDF", | |
| 987 | + "connector": "realtypress", | |
| 988 | + "status": "actif", | |
| 989 | + "type": "agence", | |
| 990 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~1 023 fiches (Thunder Bay). RealtyPress/DDF." | |
| 991 | + }, | |
| 992 | + { | |
| 993 | + "id": "rp_ag_saultstemarie", | |
| 994 | + "name": "Sault Ste. Marie Real Estate (Century 21 Choice)", | |
| 995 | + "url": "https://saultstemarierealestate.com", | |
| 996 | + "listing_url": "https://saultstemarierealestate.com/listing/", | |
| 997 | + "coverage": "Sault Ste. Marie — ~860 fiches DDF", | |
| 998 | + "connector": "realtypress", | |
| 999 | + "status": "actif", | |
| 1000 | + "type": "agence", | |
| 1001 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~864 fiches (Sault Ste. Marie). RealtyPress/DDF." | |
| 1002 | + }, | |
| 1003 | + { | |
| 1004 | + "id": "rp_ag_cbnorthbay", | |
| 1005 | + "name": "Coldwell Banker Peter Minogue (North Bay)", | |
| 1006 | + "url": "https://cbnorthbay.com", | |
| 1007 | + "listing_url": "https://cbnorthbay.com/listing/", | |
| 1008 | + "coverage": "North Bay / Nipissing — ~300 fiches DDF", | |
| 1009 | + "connector": "realtypress", | |
| 1010 | + "status": "actif", | |
| 1011 | + "type": "agence", | |
| 1012 | + "note": "Extension ONTARIO 2026-08 — connecteur générique realtypress.py (registre data/ontario_agencies.json). Plugin WordPress RealtyPress branché sur le flux CREA DDF : chaque site expose l'inventaire complet de son board. Infixe _ag_ : external_id ddf<id> partagé entre sites → la dédup masque les doublons inter-sites. Recensement ON 2026-08-27 : ~307 fiches (North Bay). RealtyPress/DDF." | |
| 870 | 1013 | } |
| 871 | 1014 | ] |
| 872 | 1015 | } |
added
immoka/connectors/realtypress.py
+292 −0
@@ -0,0 +1,292 @@ | ||
| 1 | +# ----------------------------------------------------------------------------- | |
| 2 | +# Immo-Ka — Agrégateur de maisons à vendre (Québec + Ontario) | |
| 3 | +# Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 4 | +# connectors/realtypress.py : connecteur GÉNÉRIQUE RealtyPress (Ontario) | |
| 5 | +# RealtyPress = plugin WordPress branché sur le flux CREA DDF ; ~35 sites | |
| 6 | +# d'agences/équipes ontariennes confirmés (recensement 2026-08-27, voir | |
| 7 | +# docs/ontario-agences-connecteurs.md). Chaque site expose l'IDX/DDF complet | |
| 8 | +# de son board (OREB, ITSO, KAREA…) en HTML server-rendered, sans anti-bot : | |
| 9 | +# un seul parseur couvre quasi toute la province. | |
| 10 | +# | |
| 11 | +# - liste : archive /listing/page/N/?posts_per_page=100 (100 cartes/page ; | |
| 12 | +# ⚠ ?posts_per_page directement sur /listing = 301/vide) ; cartes | |
| 13 | +# class="rps-property-result" (ruban For sale/For rent, prix, adresse, | |
| 14 | +# ville, caractéristiques) ; | |
| 15 | +# - fiche : mur CREA « I Accept The Terms » contourné par le cookie | |
| 16 | +# `disclaimer=accepted` ; tableaux <strong>Label</strong>/valeur (MLS® | |
| 17 | +# Number, Property Type, Bedrooms…), description « … (id:NNNNN) », | |
| 18 | +# lat/lng JSON-LD, photos ddfcdn.realtor.ca ; | |
| 19 | +# - external_id = ddf<id> (préfixe : jamais de collision avec les n° Centris | |
| 20 | +# QC) ; sources avec infixe _ag_ → la dédup par external_id masque les | |
| 21 | +# doublons inter-sites (le même bien DDF publié sur plusieurs sites). | |
| 22 | +# Sites générés depuis data/ontario_agencies.json (un source_id par site). | |
| 23 | +# ----------------------------------------------------------------------------- | |
| 24 | +from __future__ import annotations | |
| 25 | + | |
| 26 | +import html as _html | |
| 27 | +import json | |
| 28 | +import os | |
| 29 | +import re | |
| 30 | +import urllib.parse | |
| 31 | +from pathlib import Path | |
| 32 | + | |
| 33 | +from .base import BaseConnector | |
| 34 | +from . import _detailutil as du | |
| 35 | +from ..schema import PropertyListing | |
| 36 | + | |
| 37 | +REGISTRY = Path(__file__).resolve().parent.parent.parent / "data" / "ontario_agencies.json" | |
| 38 | +DETAIL_LIMIT = int(os.environ.get("IMMOKA_RP_DETAIL_LIMIT", | |
| 39 | + os.environ.get("IMMOKA_DETAIL_LIMIT", "150"))) | |
| 40 | + | |
| 41 | +_CARD_RE = re.compile(r'<div class="rps-property-result">') | |
| 42 | +_LINK_RE = re.compile(r'href="(https?://[^"]+/listings?/[^"]+?-(\d{6,10})/?)"') | |
| 43 | +_RIBBON_RE = re.compile(r'rps-ribbon[^>]*>\s*([^<]+?)\s*<') | |
| 44 | +_PRICE_RE = re.compile(r'rps-price[^>]*>\s*\$\s*([\d,]+)') | |
| 45 | +_H4_RE = re.compile(r"<h4>\s*(.*?)\s*</h4>", re.S) | |
| 46 | +# avec ou sans <strong> selon le thème du site | |
| 47 | +_CITY_RE = re.compile(r'city-province-postalcode[^>]*>\s*(?:<strong>\s*)?([^<]+?)\s*<', re.S) | |
| 48 | +_FEAT_RE = re.compile(r'rps-result-feature-label[^>]*>\s*([^<]+?)\s*<') | |
| 49 | +_CARD_BROKER_RE = re.compile(r'text-muted[^>]*>\s*<small>\s*([^<]+?)\s*(?:<br|</small>)', re.S) | |
| 50 | +_DDFIMG_RE = re.compile(r'https://ddfcdn\.realtor\.ca/[^")\'\s\\]+') | |
| 51 | +_ROW_RE = re.compile(r"<td[^>]*>\s*<strong>([^<]{2,45})</strong>\s*</td>\s*" | |
| 52 | + r"<td[^>]*>(.*?)</td>", re.S) | |
| 53 | +_DESC_RE = re.compile(r'<!--\s*Description\s*-->\s*<p[^>]*>(.*?)</p>', re.S) | |
| 54 | +_DESC_RE2 = re.compile(r'<p itemprop="description"[^>]*>(.*?)</p>', re.S) | |
| 55 | +# ville depuis <title> « 3383 Romeo Street, Greater Sudbury (Valley East), Ontario … » | |
| 56 | +_TITLE_CITY_RE = re.compile(r",\s*([^,<>|]{2,60}),\s*Ontario\b") | |
| 57 | +_PRICING_RE = re.compile(r'rps-pricing[^>]*>\s*\$\s*([\d,]+)') | |
| 58 | +_ID_TAIL_RE = re.compile(r"\s*\(id:\d{4,9}\)\s*$") | |
| 59 | +_TAG_RE = re.compile(r"<[^>]+>") | |
| 60 | +_NUM_RE = re.compile(r"[\d,]+(?:\.\d+)?") | |
| 61 | +_YEAR_RE = re.compile(r"\b(1[6-9]\d{2}|20\d{2})\b") | |
| 62 | +# territoire couvert (Québec + Ontario) — même boîte que schema.finalize() | |
| 63 | +_BBOX = (41.6, 63.0, -95.5, -56.0) | |
| 64 | + | |
| 65 | + | |
| 66 | +def _num(s: str) -> float | None: | |
| 67 | + m = _NUM_RE.search(s or "") | |
| 68 | + if not m: | |
| 69 | + return None | |
| 70 | + try: | |
| 71 | + return float(m.group(0).replace(",", "")) | |
| 72 | + except ValueError: | |
| 73 | + return None | |
| 74 | + | |
| 75 | + | |
| 76 | +class _RealtyPress(BaseConnector): | |
| 77 | + """Connecteur générique de site RealtyPress (voir data/ontario_agencies.json).""" | |
| 78 | + | |
| 79 | + agency_name = "" | |
| 80 | + site_url = "" | |
| 81 | + max_pages = 150 # 100 cartes/page → jusqu'à 15 000 fiches par site | |
| 82 | + request_delay = 0.6 | |
| 83 | + | |
| 84 | + def fetch(self) -> list[PropertyListing]: | |
| 85 | + # mur CREA des fiches détail : le cookie suffit (posé pour tout domaine, | |
| 86 | + # les redirections www/apex restent couvertes) | |
| 87 | + self.session.cookies.set("disclaimer", "accepted") | |
| 88 | + by_id: dict[str, PropertyListing] = {} | |
| 89 | + base = self.site_url.rstrip("/") | |
| 90 | + dry = 0 | |
| 91 | + for page in range(1, self.max_pages + 1): | |
| 92 | + url = f"{base}/listing/page/{page}/?posts_per_page=100" | |
| 93 | + try: | |
| 94 | + body = self.get(url).text | |
| 95 | + except Exception: | |
| 96 | + break | |
| 97 | + cards = self._cards(body) | |
| 98 | + if not cards: | |
| 99 | + break | |
| 100 | + before = len(by_id) | |
| 101 | + for card in cards: | |
| 102 | + self._parse_card(card, by_id) | |
| 103 | + dry = dry + 1 if len(by_id) == before else 0 | |
| 104 | + if dry >= 2: | |
| 105 | + break | |
| 106 | + listings = list(by_id.values()) | |
| 107 | + du.enrich(self, listings, DETAIL_LIMIT, parse_rp_detail, key="v1") | |
| 108 | + for lst in listings: | |
| 109 | + # n° MLS du board (fiche détail) — utile à la dédup inter-plateformes | |
| 110 | + if not lst.mls and lst.details.get("MLS® Number"): | |
| 111 | + lst.mls = str(lst.details["MLS® Number"]) | |
| 112 | + if not lst.title: | |
| 113 | + lst.title = ", ".join(filter(None, (lst.address, lst.city))) \ | |
| 114 | + or "Propriété à vendre" | |
| 115 | + return listings | |
| 116 | + | |
| 117 | + def _cards(self, body: str) -> list[str]: | |
| 118 | + marks = list(_CARD_RE.finditer(body)) | |
| 119 | + return [body[m.start():(marks[i + 1].start() if i + 1 < len(marks) | |
| 120 | + else m.start() + 6000)] | |
| 121 | + for i, m in enumerate(marks)] | |
| 122 | + | |
| 123 | + def _parse_card(self, card: str, by_id: dict) -> None: | |
| 124 | + ml = _LINK_RE.search(card) | |
| 125 | + if not ml: | |
| 126 | + return | |
| 127 | + url, ddf = ml.group(1), ml.group(2) | |
| 128 | + eid = f"ddf{ddf}" | |
| 129 | + if eid in by_id: | |
| 130 | + return | |
| 131 | + mr = _RIBBON_RE.search(card) | |
| 132 | + ribbon = (mr.group(1) if mr else "").strip().lower() | |
| 133 | + if "rent" in ribbon or "lease" in ribbon: | |
| 134 | + return # locations : hors périmètre | |
| 135 | + lst = PropertyListing(source=self.source_id, external_id=eid, url=url, | |
| 136 | + region="Ontario", agency=self.agency_name, | |
| 137 | + broker_name=self.agency_name) | |
| 138 | + ma = _H4_RE.search(card) | |
| 139 | + if ma: | |
| 140 | + lst.address = _html.unescape(_TAG_RE.sub(" ", ma.group(1))).strip() | |
| 141 | + mc = _CITY_RE.search(card) | |
| 142 | + if mc: | |
| 143 | + city = _html.unescape(mc.group(1)).strip().rstrip(",") | |
| 144 | + city = re.sub(r",?\s*Ontario\b.*$", "", city, flags=re.I) | |
| 145 | + lst.city = city.split("(")[0].strip() | |
| 146 | + mp = _PRICE_RE.search(card) | |
| 147 | + if mp: | |
| 148 | + lst.price = _num(mp.group(1)) | |
| 149 | + lst.price_label = f"{mp.group(1)} $" | |
| 150 | + for feat in _FEAT_RE.findall(card): | |
| 151 | + f = _html.unescape(feat).strip() | |
| 152 | + low = f.lower() | |
| 153 | + n = _num(f) | |
| 154 | + if not n: | |
| 155 | + continue | |
| 156 | + if "bedroom" in low: | |
| 157 | + lst.bedrooms = int(n) | |
| 158 | + elif "bathroom" in low: | |
| 159 | + lst.bathrooms = int(n) | |
| 160 | + elif "sqft" in low or "sq ft" in low or "ft" in low: | |
| 161 | + lst.area_sqft = n # plage « 1,100 - 1,500 ft² » : borne basse | |
| 162 | + mbk = _CARD_BROKER_RE.search(card) | |
| 163 | + if mbk: | |
| 164 | + lst.broker_name = _html.unescape(mbk.group(1)).strip()[:120] | |
| 165 | + mi = _DDFIMG_RE.search(card) | |
| 166 | + if mi: | |
| 167 | + lst.images = [mi.group(0)] | |
| 168 | + by_id[lst.external_id] = lst | |
| 169 | + | |
| 170 | + | |
| 171 | +def parse_rp_detail(html: str) -> dict: | |
| 172 | + """Fiche RealtyPress : tableaux DDF, description, GPS, galerie, courtier.""" | |
| 173 | + out: dict = {} | |
| 174 | + details: dict = {} | |
| 175 | + | |
| 176 | + for lab, val in _ROW_RE.findall(html): | |
| 177 | + label = _html.unescape(lab).strip().rstrip(":") | |
| 178 | + value = re.sub(r"\s+", " ", _html.unescape(_TAG_RE.sub(" ", val))).strip() | |
| 179 | + if label and value and len(value) <= 300: | |
| 180 | + details.setdefault(label, value) | |
| 181 | + | |
| 182 | + def dv(*labels: str) -> str: | |
| 183 | + for lb in labels: | |
| 184 | + if details.get(lb): | |
| 185 | + return details[lb] | |
| 186 | + return "" | |
| 187 | + | |
| 188 | + b = _num(dv("Bedrooms Total", "Bedrooms", "Bedrooms Above Ground")) | |
| 189 | + if b is not None and 0 < b <= 30: | |
| 190 | + out["bedrooms"] = int(b) | |
| 191 | + b = _num(dv("Bathroom Total", "Bathrooms")) | |
| 192 | + if b is not None and 0 < b <= 30: | |
| 193 | + out["bathrooms"] = int(b) | |
| 194 | + b = _num(dv("Half Bath Total")) | |
| 195 | + if b is not None and 0 < b <= 10: | |
| 196 | + out["powder_rooms"] = int(b) | |
| 197 | + my = _YEAR_RE.search(dv("Constructed Date", "Construction Year", "Age")) | |
| 198 | + if my: | |
| 199 | + out["year_built"] = int(my.group(1)) | |
| 200 | + si = dv("Size Interior") | |
| 201 | + if si and "sqft" in si.lower().replace(" ", ""): | |
| 202 | + a = _num(si) # « 7,901 Sqft » / « 1200 - 1399 sqft » | |
| 203 | + if a and a >= 100: | |
| 204 | + out["area_sqft"] = a | |
| 205 | + pt = dv("Property Type", "Building Type", "Type") | |
| 206 | + if pt: | |
| 207 | + out["property_type"] = pt # anglais DDF — normalisé par finalize() | |
| 208 | + sec = dv("Neigbourhood", "Neighbourhood", "Community Name") | |
| 209 | + if sec: | |
| 210 | + out["sector"] = sec | |
| 211 | + | |
| 212 | + mp = _PRICING_RE.search(html) | |
| 213 | + if mp: | |
| 214 | + out["price"] = _num(mp.group(1)) | |
| 215 | + out["price_label"] = f"{mp.group(1)} $" | |
| 216 | + | |
| 217 | + md = _DESC_RE.search(html) or _DESC_RE2.search(html) | |
| 218 | + if md: | |
| 219 | + desc = _html.unescape(_TAG_RE.sub(" ", md.group(1))) | |
| 220 | + desc = re.sub(r"\s+", " ", desc).strip() | |
| 221 | + out["description"] = _ID_TAIL_RE.sub("", desc)[:6000] | |
| 222 | + | |
| 223 | + mt = re.search(r"<title>(.*?)</title>", html, re.S) | |
| 224 | + if mt: | |
| 225 | + mc = _TITLE_CITY_RE.search(_html.unescape(mt.group(1))) | |
| 226 | + if mc: | |
| 227 | + # « Greater Sudbury (Valley East) » : le secteur part dans sector | |
| 228 | + city = mc.group(1).split("(")[0].strip() | |
| 229 | + if city and not any(c.isdigit() for c in city): | |
| 230 | + out["city"] = city | |
| 231 | + msec = re.search(r"\(([^)]{2,45})\)", mc.group(1)) | |
| 232 | + if msec and "sector" not in out: | |
| 233 | + out["sector"] = msec.group(1).strip() | |
| 234 | + | |
| 235 | + for n in du.ld_nodes(html): | |
| 236 | + t = n.get("@type") | |
| 237 | + types = set(t if isinstance(t, list) else [t]) | |
| 238 | + geo = n.get("geo") or {} | |
| 239 | + if isinstance(geo, dict) and "lat" not in out: | |
| 240 | + try: | |
| 241 | + lat, lng = float(geo["latitude"]), float(geo["longitude"]) | |
| 242 | + if _BBOX[0] <= lat <= _BBOX[1] and _BBOX[2] <= lng <= _BBOX[3]: | |
| 243 | + out["lat"], out["lng"] = lat, lng | |
| 244 | + except (KeyError, TypeError, ValueError): | |
| 245 | + pass | |
| 246 | + if types & {"RealEstateAgent", "Organization"}: | |
| 247 | + name = str(n.get("name") or "").strip() | |
| 248 | + if name and "broker_name" not in out: | |
| 249 | + out["broker_name"] = name[:120] | |
| 250 | + tel = str(n.get("telephone") or "").strip() | |
| 251 | + if tel and "broker_phone" not in out: | |
| 252 | + out["broker_phone"] = tel[:40] | |
| 253 | + if "lat" not in out: | |
| 254 | + m = re.search(r'"latitude"\s*:\s*"?(-?\d{1,2}\.\d{3,})"?\s*,\s*' | |
| 255 | + r'"longitude"\s*:\s*"?(-?\d{2,3}\.\d{3,})"?', html) | |
| 256 | + if m: | |
| 257 | + lat, lng = float(m.group(1)), float(m.group(2)) | |
| 258 | + if _BBOX[0] <= lat <= _BBOX[1] and _BBOX[2] <= lng <= _BBOX[3]: | |
| 259 | + out["lat"], out["lng"] = lat, lng | |
| 260 | + | |
| 261 | + gal = [u for u in dict.fromkeys(_DDFIMG_RE.findall(html)) | |
| 262 | + if "/listings/" in u.lower()] | |
| 263 | + if gal: | |
| 264 | + out["images"] = gal[:60] | |
| 265 | + | |
| 266 | + if details: | |
| 267 | + out["details"] = details | |
| 268 | + return out | |
| 269 | + | |
| 270 | + | |
| 271 | +def _load() -> list[dict]: | |
| 272 | + try: | |
| 273 | + return json.loads(REGISTRY.read_text(encoding="utf-8")) | |
| 274 | + except Exception: | |
| 275 | + return [] | |
| 276 | + | |
| 277 | + | |
| 278 | +# Génère une classe par site du registre. | |
| 279 | +for _ag in _load(): | |
| 280 | + if not all(_ag.get(k) for k in ("id", "site")): | |
| 281 | + continue | |
| 282 | + _sid = _ag["id"] | |
| 283 | + globals()[f"REALTYPRESS_{_sid.upper()}"] = type( | |
| 284 | + "RealtyPress" + "".join(p.title() for p in _sid.split("_")), | |
| 285 | + (_RealtyPress,), | |
| 286 | + { | |
| 287 | + "source_id": _sid, | |
| 288 | + "site_url": _ag["site"], | |
| 289 | + "agency_name": _ag.get("name", _sid), | |
| 290 | + "max_pages": int(_ag.get("max_pages", 150)), | |
| 291 | + }, | |
| 292 | + ) | |
modified
immoka/geocode.py
+34 −26
@@ -45,8 +45,11 @@ RETRY_FAILED_AFTER = 30 * 86400 # re-tenter les échecs après 30 jours | ||
| 45 | 45 | # Bounding boxes (lat_min, lat_max, lng_min, lng_max) |
| 46 | 46 | _BBOX_QUEBEC = (46.55, 47.10, -71.75, -70.85) # Québec, Lévis, St-Augustin |
| 47 | 47 | # garde-fou provincial : couvre Gatineau, l'Abitibi, le Saguenay, la Côte-Nord |
| 48 | −# et la Gaspésie (expansion provinciale) — même boîte que schema.finalize() | |
| 48 | +# et la Gaspésie (expansion provinciale) | |
| 49 | 49 | _BBOX_SUD_QC = (44.50, 63.00, -80.00, -56.00) |
| 50 | +# territoire complet Groupe Ka : Québec + Ontario (Windsor -83°, Kenora -94.5°) | |
| 51 | +# — même boîte que schema.finalize() | |
| 52 | +_BBOX_KA = (41.60, 63.00, -95.50, -56.00) | |
| 50 | 53 | |
| 51 | 54 | _VILLES_QUEBEC = {"quebec", "levis", "saint-augustin-de-desmaures", |
| 52 | 55 | "l'ancienne-lorette", "ancienne-lorette"} |
@@ -65,7 +68,7 @@ CLUSTER_MIN = 3 # une grappe de fiches devient une référence de confiance d | ||
| 65 | 68 | |
| 66 | 69 | def _bbox_for(city: str) -> tuple[float, float, float, float]: |
| 67 | 70 | key = strip_accents((city or "").strip().lower()) |
| 68 | − return _BBOX_QUEBEC if key in _VILLES_QUEBEC else _BBOX_SUD_QC | |
| 71 | + return _BBOX_QUEBEC if key in _VILLES_QUEBEC else _BBOX_KA | |
| 69 | 72 | |
| 70 | 73 | |
| 71 | 74 | def _in_bbox(lat: float, lng: float, bbox: tuple) -> bool: |
@@ -264,31 +267,35 @@ class Geocoder: | ||
| 264 | 267 | return None |
| 265 | 268 | if not allow_api: |
| 266 | 269 | return None # pas de mémoire : on réessaiera quand l'API sera permise |
| 267 | − self.ville_api_calls += 1 | |
| 268 | − wait = REQUEST_DELAY - (time.time() - self._last) | |
| 269 | − if wait > 0: | |
| 270 | − time.sleep(wait) | |
| 271 | 270 | v = None |
| 272 | − try: | |
| 273 | − resp = self.session.get(NOMINATIM_URL, params={ | |
| 274 | − "q": f"{city}, Québec, Canada", "format": "jsonv2", "limit": 1, | |
| 275 | − "countrycodes": "ca", "addressdetails": 1}, timeout=20) | |
| 276 | − self._last = time.time() | |
| 277 | − resp.raise_for_status() | |
| 278 | − hits = resp.json() | |
| 279 | − except Exception: | |
| 280 | − self._last = time.time() | |
| 281 | − return None # erreur réseau : ne pas cacher un échec définitif | |
| 282 | − if hits: | |
| 271 | + # priorité au Québec ; à défaut, l'Ontario (extension immo-ka) | |
| 272 | + for prov in ("Québec", "Ontario"): | |
| 273 | + self.ville_api_calls += 1 | |
| 274 | + wait = REQUEST_DELAY - (time.time() - self._last) | |
| 275 | + if wait > 0: | |
| 276 | + time.sleep(wait) | |
| 283 | 277 | try: |
| 284 | − lat, lng = float(hits[0]["lat"]), float(hits[0]["lon"]) | |
| 285 | − if _in_bbox(lat, lng, _BBOX_SUD_QC): | |
| 286 | − a = hits[0].get("address") or {} | |
| 287 | − muni = (a.get("city") or a.get("town") or a.get("village") | |
| 288 | − or a.get("municipality")) | |
| 289 | − v = {"lat": lat, "lng": lng, "muni": muni} | |
| 290 | − except (KeyError, ValueError, TypeError): | |
| 291 | − v = None | |
| 278 | + resp = self.session.get(NOMINATIM_URL, params={ | |
| 279 | + "q": f"{city}, {prov}, Canada", "format": "jsonv2", "limit": 1, | |
| 280 | + "countrycodes": "ca", "addressdetails": 1}, timeout=20) | |
| 281 | + self._last = time.time() | |
| 282 | + resp.raise_for_status() | |
| 283 | + hits = resp.json() | |
| 284 | + except Exception: | |
| 285 | + self._last = time.time() | |
| 286 | + return None # erreur réseau : ne pas cacher un échec définitif | |
| 287 | + if hits: | |
| 288 | + try: | |
| 289 | + lat, lng = float(hits[0]["lat"]), float(hits[0]["lon"]) | |
| 290 | + if _in_bbox(lat, lng, _BBOX_KA): | |
| 291 | + a = hits[0].get("address") or {} | |
| 292 | + muni = (a.get("city") or a.get("town") or a.get("village") | |
| 293 | + or a.get("municipality")) | |
| 294 | + v = {"lat": lat, "lng": lng, "muni": muni} | |
| 295 | + except (KeyError, ValueError, TypeError): | |
| 296 | + v = None | |
| 297 | + if v: | |
| 298 | + break | |
| 292 | 299 | self.con.execute( |
| 293 | 300 | "INSERT INTO geocode_cache (address, lat, lng, muni, provider, failed, ts)" |
| 294 | 301 | " VALUES (?,?,?,?,?,?,?) ON CONFLICT(address) DO UPDATE SET" |
@@ -474,7 +481,8 @@ def run_batch(limit: int | None = None) -> dict: | ||
| 474 | 481 | rows = con.execute( |
| 475 | 482 | """SELECT uid, address, city FROM listings |
| 476 | 483 | WHERE active=1 AND lat IS NULL AND address<>'' AND geocode_failed=0 |
| 477 | − AND dup_hidden=0 ORDER BY address""").fetchall() | |
| 484 | + AND dup_hidden=0 AND COALESCE(region,'')<>'Ontario' | |
| 485 | + ORDER BY address""").fetchall() | |
| 478 | 486 | # 1 entrée par immeuble (adresse normalisée) ; on saute les échecs en cache |
| 479 | 487 | uniq: dict[str, dict] = {} |
| 480 | 488 | for r in rows: |
modified
immoka/normalize.py
+16 −10
@@ -193,21 +193,26 @@ _TYPE_MAP = [ | ||
| 193 | 193 | # (mots-clés dans le texte source normalisé, type canonique) |
| 194 | 194 | (("plain-pied", "bungalow",), "Maison"), |
| 195 | 195 | (("maison a etages", "a etage", "deux etages", "cottage",), "Maison"), |
| 196 | − (("maison mobile", "unimodulaire",), "Maison mobile"), | |
| 197 | − (("jumele", "semi-detache", "semi detache",), "Jumelé"), | |
| 198 | − (("maison de ville", "townhouse", "en rangee",), "Maison de ville"), | |
| 199 | − (("unifamiliale", "maison", "house", "residence", "split",), "Maison"), | |
| 196 | + (("maison mobile", "unimodulaire", "mobile home", "manufactured home",), | |
| 197 | + "Maison mobile"), | |
| 198 | + (("jumele", "semi-detache", "semi detache", "semi-detached", "semi detached",), | |
| 199 | + "Jumelé"), | |
| 200 | + (("maison de ville", "townhouse", "en rangee", "row house", "row / town",), | |
| 201 | + "Maison de ville"), | |
| 202 | + (("unifamiliale", "maison", "house", "residence", "split", "detached", | |
| 203 | + "single family",), "Maison"), | |
| 200 | 204 | (("condo", "copropriete", "appartement", "apartment", "loft", "penthouse", |
| 201 | 205 | "studio",), "Condo"), |
| 202 | 206 | (("duplex",), "Duplex"), |
| 203 | 207 | (("triplex",), "Triplex"), |
| 204 | 208 | (("quadruplex", "quintuplex", "multiplex", "multilogement", "multi-logement", |
| 205 | − "immeuble a revenus", "revenus",), "Multiplex"), | |
| 206 | − (("chalet", "cottage 4 saisons", "acces au plan d'eau", "bord de l'eau",), | |
| 207 | − "Chalet"), | |
| 209 | + "immeuble a revenus", "revenus", "multi-family", "multi family", | |
| 210 | + "fourplex",), "Multiplex"), | |
| 211 | + (("chalet", "cottage 4 saisons", "acces au plan d'eau", "bord de l'eau", | |
| 212 | + "recreational",), "Chalet"), | |
| 208 | 213 | (("terre", "terrain", "lot ", "land",), "Terrain"), |
| 209 | − (("ferme", "fermette", "agricole", "agriculture", "hobby farm",), | |
| 210 | − "Fermette/Agricole"), | |
| 214 | + (("ferme", "fermette", "agricole", "agriculture", "hobby farm", "farm", | |
| 215 | + "acreage",), "Fermette/Agricole"), | |
| 211 | 216 | # vocabulaire des fiches enrichies : « 4 logements », « propriété à revenu » |
| 212 | 217 | (("logements", "logement/", "unites et +", "revenu",), "Multiplex"), |
| 213 | 218 | (("bi generation", "bi-generation", "bigeneration", "intergeneration",), |
@@ -215,7 +220,8 @@ _TYPE_MAP = [ | ||
| 215 | 220 | (("domaine et villa", "villa", "domaine",), "Maison"), |
| 216 | 221 | (("commercial", "commerce", "industriel", "industrie", "bureau", "local", |
| 217 | 222 | "entreprise", "batisse", "restaurant", "depanneur", "hotel", "motel", |
| 218 | − "garage/", "concessionnaire", "coiffure", "esthetique", "camping",), | |
| 223 | + "garage/", "concessionnaire", "coiffure", "esthetique", "camping", | |
| 224 | + "retail", "office", "industrial", "warehouse", "business",), | |
| 219 | 225 | "Commercial"), |
| 220 | 226 | ] |
| 221 | 227 | |
modified
immoka/schema.py
+3 −3
@@ -134,10 +134,10 @@ class PropertyListing: | ||
| 134 | 134 | if not self.agency: |
| 135 | 135 | self.agency = self.broker_name |
| 136 | 136 | |
| 137 | − # coordonnées fournies par la source : rejeter tout point hors de la | |
| 138 | − # province (lat/lng inversés, 0/0, coquilles) | |
| 137 | + # coordonnées fournies par la source : rejeter tout point hors du | |
| 138 | + # territoire couvert — Québec + Ontario (lat/lng inversés, 0/0, coquilles) | |
| 139 | 139 | if self.lat is not None and self.lng is not None: |
| 140 | − if not (44.5 <= self.lat <= 63.0 and -80.0 <= self.lng <= -56.0): | |
| 140 | + if not (41.6 <= self.lat <= 63.0 and -95.5 <= self.lng <= -56.0): | |
| 141 | 141 | self.lat = self.lng = None |
| 142 | 142 | |
| 143 | 143 | return self |
| 144 | 144 | |