# ============================================================================== # Author: Simon-Pierre Boucher # File: restoka/regions.py # Desc: Rattachement obligatoire de chaque restaurant à l'une des 17 régions # administratives du Québec : table ville → région, repli par préfixe # de code postal, puis par coordonnées (boîtes englobantes). # ============================================================================== from __future__ import annotations import re import unicodedata REGIONS = [ "Bas-Saint-Laurent", "Saguenay–Lac-Saint-Jean", "Capitale-Nationale", "Mauricie", "Estrie", "Montréal", "Outaouais", "Abitibi-Témiscamingue", "Côte-Nord", "Nord-du-Québec", "Gaspésie–Îles-de-la-Madeleine", "Chaudière-Appalaches", "Laval", "Lanaudière", "Laurentides", "Montérégie", "Centre-du-Québec", ] def strip_accents(text: str) -> str: return "".join(c for c in unicodedata.normalize("NFD", text) if unicodedata.category(c) != "Mn") def _key(city: str) -> str: s = strip_accents((city or "").strip().lower()) s = re.sub(r"^(ville de|city of)\s+", "", s) s = re.sub(r"\s*\(.*\)$", "", s) # « Fiset Sorel-Tracy (arr.) » s = re.sub(r"[^a-z0-9]+", "-", s) return s.strip("-") # Ville (clé normalisée) → région. Municipalités principales + arrondissements # fréquents dans les adresses des plateformes. Étendre au besoin (une ville # inconnue n'est jamais inventée : repli code postal, puis coordonnées). VILLE_REGION: dict[str, str] = { # --- Montréal (île) ------------------------------------------------------- **{v: "Montréal" for v in ( "montreal", "montreal-est", "montreal-ouest", "montreal-nord", "anjou", "lasalle", "lachine", "verdun", "outremont", "westmount", "saint-leonard", "saint-laurent", "ahuntsic", "ahuntsic-cartierville", "rosemont", "villeray", "hochelaga-maisonneuve", "mercier", "riviere-des-prairies", "pointe-aux-trembles", "pierrefonds", "roxboro", "dollard-des-ormeaux", "pointe-claire", "kirkland", "beaconsfield", "baie-d-urfe", "sainte-anne-de-bellevue", "senneville", "dorval", "mont-royal", "ville-mont-royal", "cote-saint-luc", "hampstead", "montreal-royal", "le-plateau-mont-royal", "griffintown", "ville-marie", "cote-des-neiges", "notre-dame-de-grace", "ile-bizard", "sainte-genevieve", )}, # --- Laval ---------------------------------------------------------------- **{v: "Laval" for v in ( "laval", "chomedey", "sainte-rose", "vimont", "duvernay", "fabreville", "sainte-dorothee", "pont-viau", "laval-des-rapides", "auteuil", )}, # --- Capitale-Nationale --------------------------------------------------- **{v: "Capitale-Nationale" for v in ( "quebec", "l-ancienne-lorette", "ancienne-lorette", "saint-augustin-de-desmaures", "sainte-foy", "beauport", "charlesbourg", "loretteville", "val-belair", "cap-rouge", "sillery", "limoilou", "wendake", "boischatel", "l-ange-gardien", "chateau-richer", "sainte-anne-de-beaupre", "beaupre", "saint-ferreol-les-neiges", "baie-saint-paul", "la-malbaie", "clermont", "saint-simeon", "petite-riviere-saint-francois", "donnacona", "pont-rouge", "saint-raymond", "portneuf", "cap-sante", "neuville", "lac-beauport", "stoneham", "stoneham-et-tewkesbury", "deschambault", "deschambault-grondines", "sainte-catherine-de-la-jacques-cartier", "shannon", "sainte-brigitte-de-laval", "ile-d-orleans", "saint-pierre-de-l-ile-d-orleans", )}, # --- Chaudière-Appalaches ------------------------------------------------- **{v: "Chaudière-Appalaches" for v in ( "levis", "saint-romuald", "charny", "saint-nicolas", "saint-jean-chrysostome", "pintendre", "saint-redempteur", "breakeyville", "saint-etienne-de-lauzon", "saint-henri", "montmagny", "l-islet", "saint-jean-port-joli", "sainte-marie", "saint-georges", "saint-joseph-de-beauce", "beauceville", "thetford-mines", "lac-etchemin", "saint-anselme", "scott", "saint-come-liniere", "st-georges-de-beauce", "saint-georges-de-beauce", "vallee-jonction", "saint-lambert-de-lauzon", "laurier-station", "saint-apollinaire", "saint-agapit", )}, # --- Montérégie ----------------------------------------------------------- **{v: "Montérégie" for v in ( "longueuil", "brossard", "saint-lambert", "greenfield-park", "saint-hubert", "boucherville", "saint-bruno-de-montarville", "sainte-julie", "varennes", "vercheres", "contrecoeur", "beloeil", "mont-saint-hilaire", "otterburn-park", "mcmasterville", "saint-basile-le-grand", "chambly", "carignan", "richelieu", "marieville", "saint-jean-sur-richelieu", "iberville", "saint-luc", "granby", "waterloo", "cowansville", "bromont", "farnham", "salaberry-de-valleyfield", "vaudreuil-dorion", "l-ile-perrot", "pincourt", "saint-lazare", "hudson", "rigaud", "les-coteaux", "coteau-du-lac", "beauharnois", "chateauguay", "mercier-monteregie", "sainte-catherine", "saint-constant", "delson", "candiac", "la-prairie", "saint-philippe", "saint-remi", "napierville", "sorel-tracy", "fiset-sorel-tracy", "sainte-anne-de-sorel", "saint-hyacinthe", "sainte-helene-de-bagot", "acton-vale", "lac-brome", "sutton-monteregie", "huntingdon", "ormstown", "kahnawake", "saint-amable", )}, # --- Estrie ---------------------------------------------------------------- **{v: "Estrie" for v in ( "sherbrooke", "magog", "orford", "north-hatley", "ayer-s-cliff", "coaticook", "stanstead", "rock-forest", "fleurimont", "lennoxville", "windsor", "val-des-sources", "asbestos", "danville", "richmond", "east-angus", "cookshire-eaton", "lac-megantic", "sutton", "knowlton", "eastman", "valcourt", "waterville", "compton", )}, # --- Mauricie --------------------------------------------------------------- **{v: "Mauricie" for v in ( "trois-rivieres", "cap-de-la-madeleine", "shawinigan", "grand-mere", "la-tuque", "louiseville", "yamachiche", "saint-tite", "sainte-anne-de-la-perade", "batiscan", "nicolet-mauricie", "maskinonge", "saint-boniface", "saint-etienne-des-gres", "notre-dame-du-mont-carmel", )}, # --- Centre-du-Québec ------------------------------------------------------- **{v: "Centre-du-Québec" for v in ( "drummondville", "victoriaville", "becancour", "nicolet", "plessisville", "princeville", "warwick", "kingsey-falls", "saint-cyrille-de-wendover", "gentilly", "daveluyville", )}, # --- Lanaudière ------------------------------------------------------------- **{v: "Lanaudière" for v in ( "terrebonne", "mascouche", "repentigny", "l-assomption", "charlemagne", "lachenaie", "la-plaine", "joliette", "notre-dame-des-prairies", "saint-charles-borromee", "berthierville", "lavaltrie", "lanoraie", "rawdon", "saint-donat", "saint-michel-des-saints", "saint-lin-laurentides", "saint-roch-de-l-achigan", "l-epiphanie", "saint-sulpice", )}, # --- Laurentides ------------------------------------------------------------ **{v: "Laurentides" for v in ( "saint-jerome", "saint-antoine", "bellefeuille", "lafontaine", "blainville", "boisbriand", "sainte-therese", "rosemere", "lorraine", "bois-des-filion", "deux-montagnes", "saint-eustache", "mirabel", "oka", "pointe-calumet", "sainte-marthe-sur-le-lac", "saint-joseph-du-lac", "prevost", "saint-sauveur", "saint-sauveur-des-monts", "piedmont", "sainte-adele", "morin-heights", "sainte-agathe-des-monts", "val-david", "val-morin", "saint-donat-laurentides", "mont-tremblant", "labelle", "mont-laurier", "riviere-rouge", "sainte-anne-des-plaines", "lachute", "brownsburg-chatham", "grenville", )}, # --- Outaouais -------------------------------------------------------------- **{v: "Outaouais" for v in ( "gatineau", "hull", "aylmer", "buckingham", "masson-angers", "chelsea", "cantley", "wakefield", "la-peche", "val-des-monts", "papineauville", "montebello", "thurso", "maniwaki", "fort-coulonge", "shawville", "campbell-s-bay", )}, # --- Abitibi-Témiscamingue --------------------------------------------------- **{v: "Abitibi-Témiscamingue" for v in ( "rouyn-noranda", "val-d-or", "amos", "la-sarre", "malartic", "riviere-heva", "senneterre", "ville-marie-temiscamingue", "temiscaming", "macamic", )}, # --- Saguenay–Lac-Saint-Jean ------------------------------------------------- **{v: "Saguenay–Lac-Saint-Jean" for v in ( "saguenay", "chicoutimi", "jonquiere", "la-baie", "laterriere", "alma", "roberval", "saint-felicien", "dolbeau-mistassini", "normandin", "saint-prime", "metabetchouan", "hebertville", "saint-ambroise", "saint-honore", "l-anse-saint-jean", "petit-saguenay", )}, # --- Bas-Saint-Laurent ------------------------------------------------------- **{v: "Bas-Saint-Laurent" for v in ( "rimouski", "riviere-du-loup", "matane", "mont-joli", "amqui", "causapscal", "trois-pistoles", "temiscouata-sur-le-lac", "cabano", "degelis", "pohenegamook", "la-pocatiere", "saint-pascal", "kamouraska", "le-bic", "sainte-luce", "sainte-flavie", "saint-fabien", )}, # --- Gaspésie–Îles-de-la-Madeleine -------------------------------------------- **{v: "Gaspésie–Îles-de-la-Madeleine" for v in ( "gaspe", "perce", "chandler", "grande-riviere", "paspebiac", "bonaventure", "new-richmond", "carleton-sur-mer", "maria", "matapedia", "sainte-anne-des-monts", "cap-chat", "murdochville", "les-iles-de-la-madeleine", "cap-aux-meules", "havre-aubert", )}, # --- Côte-Nord ---------------------------------------------------------------- **{v: "Côte-Nord" for v in ( "baie-comeau", "sept-iles", "port-cartier", "forestville", "les-escoumins", "tadoussac", "bergeronnes", "havre-saint-pierre", "fermont", "blanc-sablon", "natashquan", "chute-aux-outardes", )}, # --- Nord-du-Québec ------------------------------------------------------------- **{v: "Nord-du-Québec" for v in ( "chibougamau", "chapais", "lebel-sur-quevillon", "matagami", "radisson", "kuujjuaq", "mistissini", "waskaganish", )}, } # Repli nº 1 : préfixe de code postal (3 caractères) → région. Couverture # partielle mais fiable (les préfixes listés sont sans ambiguïté). _POSTAL_REGION = { "H7": "Laval", # Laval au complet "G1": "Capitale-Nationale", "G2": "Capitale-Nationale", "G3": "Capitale-Nationale", "G6": "Chaudière-Appalaches", "G5V": "Chaudière-Appalaches", "G5X": "Chaudière-Appalaches", "G7": "Saguenay–Lac-Saint-Jean", "G8": "Saguenay–Lac-Saint-Jean", "G5L": "Bas-Saint-Laurent", "G5R": "Bas-Saint-Laurent", "G4X": "Gaspésie–Îles-de-la-Madeleine", "G4W": "Bas-Saint-Laurent", "G4R": "Côte-Nord", "G4Z": "Côte-Nord", "G5C": "Côte-Nord", "G9A": "Mauricie", "G8T": "Mauricie", "G9N": "Mauricie", "G9H": "Centre-du-Québec", "J1": "Estrie", "J2": "Montérégie", "J3": "Montérégie", "J4": "Montérégie", "J5R": "Montérégie", "J6": "Montérégie", "J7": "Laurentides", "J8": "Outaouais", "J9": "Abitibi-Témiscamingue", "J9B": "Outaouais", "J5W": "Lanaudière", "J6E": "Lanaudière", } # Repli nº 2 : ancres géographiques (lat, lng) des principaux pôles de chaque # région — le point le plus proche gagne. Plus robuste que des boîtes # englobantes qui se chevauchent ; couvre les 17 régions. Utilisé seulement # quand ni la ville ni le code postal ne tranchent (POI ruraux d'OSM, etc.). _REGION_ANCHORS: list[tuple[str, float, float]] = [ ("Montréal", 45.55, -73.65), ("Montréal", 45.47, -73.82), ("Laval", 45.585, -73.75), ("Montérégie", 45.50, -73.42), ("Montérégie", 45.30, -73.26), ("Montérégie", 45.40, -74.03), ("Montérégie", 45.05, -74.13), ("Montérégie", 45.63, -72.95), ("Montérégie", 46.04, -73.11), ("Montérégie", 45.40, -72.73), ("Lanaudière", 45.75, -73.45), ("Lanaudière", 46.02, -73.44), ("Lanaudière", 46.35, -73.87), ("Lanaudière", 46.02, -73.78), ("Laurentides", 45.78, -74.00), ("Laurentides", 45.88, -74.17), ("Laurentides", 46.12, -74.60), ("Laurentides", 46.55, -75.50), ("Laurentides", 45.65, -74.34), ("Outaouais", 45.48, -75.70), ("Outaouais", 45.65, -76.24), ("Outaouais", 46.38, -75.98), ("Outaouais", 45.88, -77.10), ("Abitibi-Témiscamingue", 48.24, -79.03), ("Abitibi-Témiscamingue", 48.10, -77.78), ("Abitibi-Témiscamingue", 48.57, -78.12), ("Abitibi-Témiscamingue", 47.34, -79.44), ("Estrie", 45.40, -71.90), ("Estrie", 45.27, -72.15), ("Estrie", 45.60, -71.00), ("Estrie", 45.03, -72.10), ("Estrie", 45.77, -71.62), ("Centre-du-Québec", 45.88, -72.48), ("Centre-du-Québec", 46.06, -71.96), ("Centre-du-Québec", 46.22, -72.62), ("Mauricie", 46.35, -72.55), ("Mauricie", 46.57, -72.75), ("Mauricie", 47.44, -72.78), ("Mauricie", 46.28, -72.94), ("Capitale-Nationale", 46.81, -71.22), ("Capitale-Nationale", 46.94, -71.00), ("Capitale-Nationale", 46.68, -71.75), ("Capitale-Nationale", 47.44, -70.50), ("Capitale-Nationale", 46.86, -71.90), ("Chaudière-Appalaches", 46.74, -71.28), ("Chaudière-Appalaches", 46.12, -70.67), ("Chaudière-Appalaches", 46.05, -71.30), ("Chaudière-Appalaches", 46.98, -70.55), ("Chaudière-Appalaches", 46.55, -70.88), ("Saguenay–Lac-Saint-Jean", 48.43, -71.07), ("Saguenay–Lac-Saint-Jean", 48.55, -71.65), ("Saguenay–Lac-Saint-Jean", 48.51, -72.23), ("Saguenay–Lac-Saint-Jean", 48.88, -72.23), ("Bas-Saint-Laurent", 48.45, -68.53), ("Bas-Saint-Laurent", 47.83, -69.53), ("Bas-Saint-Laurent", 48.85, -67.53), ("Bas-Saint-Laurent", 47.37, -70.03), ("Gaspésie–Îles-de-la-Madeleine", 48.83, -64.48), ("Gaspésie–Îles-de-la-Madeleine", 48.40, -64.68), ("Gaspésie–Îles-de-la-Madeleine", 48.10, -66.10), ("Gaspésie–Îles-de-la-Madeleine", 49.13, -66.50), ("Gaspésie–Îles-de-la-Madeleine", 47.38, -61.87), ("Côte-Nord", 49.22, -68.15), ("Côte-Nord", 50.22, -66.38), ("Côte-Nord", 48.15, -69.72), ("Côte-Nord", 50.28, -63.60), ("Nord-du-Québec", 49.92, -74.37), ("Nord-du-Québec", 49.78, -77.63), ("Nord-du-Québec", 53.79, -73.67), ("Nord-du-Québec", 58.10, -68.40), ] def region_from_latlng(lat: float, lng: float) -> str: """Région la plus proche par ancre (approximatif — dernier recours).""" best, best_d2 = "", float("inf") for name, alat, alng in _REGION_ANCHORS: d2 = (lat - alat) ** 2 + ((lng - alng) * 0.7) ** 2 # ~pondération lng if d2 < best_d2: best, best_d2 = name, d2 return best def attach_region(city: str, postal_code: str = "", lat: float | None = None, lng: float | None = None) -> str: """Région administrative d'un restaurant. Retourne "" si indéterminable (jamais de région inventée).""" region = VILLE_REGION.get(_key(city)) if region: return region pc = (postal_code or "").replace(" ", "").upper() if pc: for prefix in (pc[:3], pc[:2]): if prefix in _POSTAL_REGION: return _POSTAL_REGION[prefix] if lat is not None and lng is not None: return region_from_latlng(lat, lng) return ""