# Author: Simon-Pierre Boucher # Contact: contact@spboucher.ai """Normalisation pour la résolution d'entités et le regroupement géographique. - normalize_name : clé canonique d'un nom (accents, ponctuation, suffixes légaux retirés). - canonical_location : regroupe les variantes ("Montreal"/"Montréal"/"Montréal, QC") en un libellé unique, et rattache les grandes villes du Québec à un libellé stable. - domain : domaine racine d'une URL (pour relier des entités du même site). """ from __future__ import annotations import re import unicodedata from urllib.parse import urlparse _LEGAL = { "inc", "ltee", "ltd", "limited", "limitee", "enr", "senc", "sencrl", "srl", "corp", "corporation", "cie", "co", "llc", "llp", "sec", "sa", "sas", } def strip_accents(s: str) -> str: return "".join(c for c in unicodedata.normalize("NFD", s or "") if unicodedata.category(c) != "Mn") def normalize_name(name: str) -> str: """Clé de résolution : minuscules, sans accents/ponctuation ni suffixe légal.""" s = strip_accents((name or "").lower()) s = re.sub(r"[^a-z0-9 ]+", " ", s) toks = [t for t in s.split() if t and t not in _LEGAL] return " ".join(toks).strip() def domain(url: str | None) -> str: if not url: return "" u = url.strip() if "://" not in u: u = "http://" + u try: net = urlparse(u).netloc.lower() except Exception: return "" if net.startswith("www."): net = net[4:] return net # Grandes villes / variantes fréquentes -> libellé canonique _CITY_MAP = { "montreal": "Montréal", "mtl": "Montréal", "ville-marie": "Montréal", "quebec": "Québec", "quebec city": "Québec", "ville de quebec": "Québec", "laval": "Laval", "gatineau": "Gatineau", "hull": "Gatineau", "sherbrooke": "Sherbrooke", "trois rivieres": "Trois-Rivières", "saguenay": "Saguenay", "chicoutimi": "Saguenay", "jonquiere": "Saguenay", "levis": "Lévis", "longueuil": "Longueuil", "terrebonne": "Terrebonne", "brossard": "Brossard", "repentigny": "Repentigny", "drummondville": "Drummondville", "saint jean sur richelieu": "Saint-Jean-sur-Richelieu", "granby": "Granby", "blainville": "Blainville", "saint jerome": "Saint-Jérôme", "mirabel": "Mirabel", "rimouski": "Rimouski", "victoriaville": "Victoriaville", "shawinigan": "Shawinigan", "rouyn noranda": "Rouyn-Noranda", "sept iles": "Sept-Îles", "val d or": "Val-d'Or", "boucherville": "Boucherville", "mascouche": "Mascouche", "salaberry": "Salaberry-de-Valleyfield", "chateauguay": "Châteauguay", "saint hyacinthe": "Saint-Hyacinthe", "sorel": "Sorel-Tracy", "joliette": "Joliette", "magog": "Magog", "alma": "Alma", "thetford": "Thetford Mines", } def canonical_location(text: str | None) -> str: """Retourne un libellé de lieu canonique (ville QC connue, sinon 1er segment nettoyé).""" if not text: return "" base = strip_accents(text.lower()) base = re.sub(r"[^a-z0-9 ]+", " ", base) base = re.sub(r"\s+", " ", base).strip() for kw, label in _CITY_MAP.items(): if re.search(rf"\b{re.escape(kw)}\b", base): return label # sinon : 1er segment avant virgule, sans mentions province/pays seg = text.split(",")[0].strip() seg = re.sub(r"(?i)\b(qc|québec|quebec|canada)\b", "", seg).strip(" ,-") return seg[:60]