Normalisation phase 2 : bornes/réinterprétation des salaires structurés (sanitize_salary), extraction salariale par phrases avec garde-fou de contexte (financement/remboursements), clean_html itératif (contenus sur-échappés), détection de langue fr/en/bilingue, extraction des blocs Avantages, titre d'affichage normalisé (title_clean), régions administratives MAMH (17 régions), quarantaine qualité (jobka/quality.py) + colonnes company_logo/language/apply_url/title_clean/quarantine, secours cache détail périmé, expiration des offres orphelines
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8 changed files +1,919 −18
added
data/regions-qc.json
+1261 −0
@@ -0,0 +1,1261 @@ | ||
| 1 | +{ | |
| 2 | +"abercorn": "Estrie", | |
| 3 | +"acton vale": "Montérégie", | |
| 4 | +"adstock": "Chaudière-Appalaches", | |
| 5 | +"aguanish": "Côte-Nord", | |
| 6 | +"ahuntsic": "Montréal", | |
| 7 | +"akulivik": "Nord-du-Québec", | |
| 8 | +"akwesasne": "Montérégie", | |
| 9 | +"albanel": "Saguenay–Lac-Saint-Jean", | |
| 10 | +"albertville": "Bas-Saint-Laurent", | |
| 11 | +"alleyn-et-cawood": "Outaouais", | |
| 12 | +"alma": "Saguenay–Lac-Saint-Jean", | |
| 13 | +"amherst": "Laurentides", | |
| 14 | +"amos": "Abitibi-Témiscamingue", | |
| 15 | +"amqui": "Bas-Saint-Laurent", | |
| 16 | +"ange-gardien": "Montérégie", | |
| 17 | +"anjou": "Montréal", | |
| 18 | +"armagh": "Chaudière-Appalaches", | |
| 19 | +"arundel": "Laurentides", | |
| 20 | +"ascot corner": "Estrie", | |
| 21 | +"aston-jonction": "Centre-du-Québec", | |
| 22 | +"auclair": "Bas-Saint-Laurent", | |
| 23 | +"audet": "Estrie", | |
| 24 | +"aumond": "Outaouais", | |
| 25 | +"aupaluk": "Nord-du-Québec", | |
| 26 | +"austin": "Estrie", | |
| 27 | +"authier": "Abitibi-Témiscamingue", | |
| 28 | +"authier-nord": "Abitibi-Témiscamingue", | |
| 29 | +"ayer's cliff": "Estrie", | |
| 30 | +"aylmer": "Outaouais", | |
| 31 | +"baie-atibenne": "Lanaudière", | |
| 32 | +"baie-comeau": "Côte-Nord", | |
| 33 | +"baie-d'hudson": "Nord-du-Québec", | |
| 34 | +"baie-d'urfe": "Montréal", | |
| 35 | +"baie-de-la-bouteille": "Lanaudière", | |
| 36 | +"baie-des-chaloupes": "Laurentides", | |
| 37 | +"baie-des-sables": "Bas-Saint-Laurent", | |
| 38 | +"baie-du-febvre": "Centre-du-Québec", | |
| 39 | +"baie-johan-beetz": "Côte-Nord", | |
| 40 | +"baie-obaoca": "Lanaudière", | |
| 41 | +"baie-saint-paul": "Capitale-Nationale", | |
| 42 | +"baie-sainte-catherine": "Capitale-Nationale", | |
| 43 | +"baie-trinite": "Côte-Nord", | |
| 44 | +"barkmere": "Laurentides", | |
| 45 | +"barnston-ouest": "Estrie", | |
| 46 | +"barraute": "Abitibi-Témiscamingue", | |
| 47 | +"batiscan": "Mauricie", | |
| 48 | +"beaconsfield": "Montréal", | |
| 49 | +"bearn": "Abitibi-Témiscamingue", | |
| 50 | +"beauceville": "Chaudière-Appalaches", | |
| 51 | +"beauharnois": "Montérégie", | |
| 52 | +"beaulac-garthby": "Chaudière-Appalaches", | |
| 53 | +"beaumont": "Chaudière-Appalaches", | |
| 54 | +"beauport": "Capitale-Nationale", | |
| 55 | +"beaupre": "Capitale-Nationale", | |
| 56 | +"becancour": "Centre-du-Québec", | |
| 57 | +"bedford": "Estrie", | |
| 58 | +"begin": "Saguenay–Lac-Saint-Jean", | |
| 59 | +"belcourt": "Abitibi-Témiscamingue", | |
| 60 | +"belle-riviere": "Saguenay–Lac-Saint-Jean", | |
| 61 | +"belleterre": "Abitibi-Témiscamingue", | |
| 62 | +"beloeil": "Montérégie", | |
| 63 | +"berry": "Abitibi-Témiscamingue", | |
| 64 | +"berthier-sur-mer": "Chaudière-Appalaches", | |
| 65 | +"berthierville": "Lanaudière", | |
| 66 | +"bethanie": "Montérégie", | |
| 67 | +"biencourt": "Bas-Saint-Laurent", | |
| 68 | +"blainville": "Laurentides", | |
| 69 | +"blanc-sablon": "Côte-Nord", | |
| 70 | +"blue sea": "Outaouais", | |
| 71 | +"boileau": "Outaouais", | |
| 72 | +"bois-des-filion": "Laurentides", | |
| 73 | +"bois-franc": "Outaouais", | |
| 74 | +"boisbriand": "Laurentides", | |
| 75 | +"boischatel": "Capitale-Nationale", | |
| 76 | +"bolton-est": "Estrie", | |
| 77 | +"bolton-ouest": "Estrie", | |
| 78 | +"bonaventure": "Gaspésie–Îles-de-la-Madeleine", | |
| 79 | +"bonne-esperance": "Côte-Nord", | |
| 80 | +"bonsecours": "Estrie", | |
| 81 | +"boucherville": "Montérégie", | |
| 82 | +"bouchette": "Outaouais", | |
| 83 | +"bowman": "Outaouais", | |
| 84 | +"brebeuf": "Laurentides", | |
| 85 | +"brigham": "Estrie", | |
| 86 | +"bristol": "Outaouais", | |
| 87 | +"brome": "Estrie", | |
| 88 | +"bromont": "Estrie", | |
| 89 | +"brossard": "Montérégie", | |
| 90 | +"brownsburg-chatham": "Laurentides", | |
| 91 | +"bryson": "Outaouais", | |
| 92 | +"buckingham": "Outaouais", | |
| 93 | +"bury": "Estrie", | |
| 94 | +"cacouna": "Bas-Saint-Laurent", | |
| 95 | +"calixa-lavallee": "Montérégie", | |
| 96 | +"campbell's bay": "Outaouais", | |
| 97 | +"candiac": "Montérégie", | |
| 98 | +"caniapiscau": "Côte-Nord", | |
| 99 | +"cantley": "Outaouais", | |
| 100 | +"cap-chat": "Gaspésie–Îles-de-la-Madeleine", | |
| 101 | +"cap-de-la-madeleine": "Mauricie", | |
| 102 | +"cap-rouge": "Capitale-Nationale", | |
| 103 | +"cap-saint-ignace": "Chaudière-Appalaches", | |
| 104 | +"cap-sante": "Capitale-Nationale", | |
| 105 | +"caplan": "Gaspésie–Îles-de-la-Madeleine", | |
| 106 | +"carignan": "Montérégie", | |
| 107 | +"carleton-sur-mer": "Gaspésie–Îles-de-la-Madeleine", | |
| 108 | +"cascades-malignes": "Outaouais", | |
| 109 | +"cascapedia--saint-jules": "Gaspésie–Îles-de-la-Madeleine", | |
| 110 | +"causapscal": "Bas-Saint-Laurent", | |
| 111 | +"cayamant": "Outaouais", | |
| 112 | +"chambly": "Montérégie", | |
| 113 | +"chambord": "Saguenay–Lac-Saint-Jean", | |
| 114 | +"champlain": "Mauricie", | |
| 115 | +"champneuf": "Abitibi-Témiscamingue", | |
| 116 | +"chandler": "Gaspésie–Îles-de-la-Madeleine", | |
| 117 | +"chapais": "Nord-du-Québec", | |
| 118 | +"charette": "Mauricie", | |
| 119 | +"charlemagne": "Lanaudière", | |
| 120 | +"charlesbourg": "Capitale-Nationale", | |
| 121 | +"charny": "Chaudière-Appalaches", | |
| 122 | +"chartierville": "Estrie", | |
| 123 | +"chateau-richer": "Capitale-Nationale", | |
| 124 | +"chateauguay": "Montérégie", | |
| 125 | +"chazel": "Abitibi-Témiscamingue", | |
| 126 | +"chelsea": "Outaouais", | |
| 127 | +"cheneville": "Outaouais", | |
| 128 | +"chertsey": "Lanaudière", | |
| 129 | +"chesterville": "Centre-du-Québec", | |
| 130 | +"chibougamau": "Nord-du-Québec", | |
| 131 | +"chichester": "Outaouais", | |
| 132 | +"chicoutimi": "Saguenay–Lac-Saint-Jean", | |
| 133 | +"chisasibi": "Nord-du-Québec", | |
| 134 | +"chute-aux-outardes": "Côte-Nord", | |
| 135 | +"chute-saint-philippe": "Laurentides", | |
| 136 | +"clarenceville": "Montérégie", | |
| 137 | +"clarendon": "Outaouais", | |
| 138 | +"clermont": "Capitale-Nationale", | |
| 139 | +"clerval": "Abitibi-Témiscamingue", | |
| 140 | +"cleveland": "Estrie", | |
| 141 | +"cloridorme": "Gaspésie–Îles-de-la-Madeleine", | |
| 142 | +"coaticook": "Estrie", | |
| 143 | +"collines-du-basque": "Gaspésie–Îles-de-la-Madeleine", | |
| 144 | +"colombier": "Côte-Nord", | |
| 145 | +"compton": "Estrie", | |
| 146 | +"contrecoeur": "Montérégie", | |
| 147 | +"cookshire-eaton": "Estrie", | |
| 148 | +"cote-nord-du-golfe-du-saint-laurent": "Côte-Nord", | |
| 149 | +"cote-saint-luc": "Montréal", | |
| 150 | +"coteau-du-lac": "Montérégie", | |
| 151 | +"coucoucache": "Mauricie", | |
| 152 | +"coulee-des-adolphe": "Gaspésie–Îles-de-la-Madeleine", | |
| 153 | +"courcelles--saint-evariste": "Chaudière-Appalaches", | |
| 154 | +"cowansville": "Estrie", | |
| 155 | +"crabtree": "Lanaudière", | |
| 156 | +"danville": "Estrie", | |
| 157 | +"daveluyville": "Centre-du-Québec", | |
| 158 | +"degelis": "Bas-Saint-Laurent", | |
| 159 | +"deleage": "Outaouais", | |
| 160 | +"delson": "Montérégie", | |
| 161 | +"denholm": "Outaouais", | |
| 162 | +"depot-echouani": "Outaouais", | |
| 163 | +"desbiens": "Saguenay–Lac-Saint-Jean", | |
| 164 | +"deschaillons-sur-saint-laurent": "Centre-du-Québec", | |
| 165 | +"deschambault-grondines": "Capitale-Nationale", | |
| 166 | +"deux-montagnes": "Laurentides", | |
| 167 | +"disraeli": "Chaudière-Appalaches", | |
| 168 | +"dixville": "Estrie", | |
| 169 | +"dolbeau-mistassini": "Saguenay–Lac-Saint-Jean", | |
| 170 | +"dollard-des-ormeaux": "Montréal", | |
| 171 | +"doncaster": "Laurentides", | |
| 172 | +"donnacona": "Capitale-Nationale", | |
| 173 | +"dorval": "Montréal", | |
| 174 | +"dosquet": "Chaudière-Appalaches", | |
| 175 | +"drummondville": "Centre-du-Québec", | |
| 176 | +"dudswell": "Estrie", | |
| 177 | +"duhamel": "Outaouais", | |
| 178 | +"duhamel-ouest": "Abitibi-Témiscamingue", | |
| 179 | +"dundee": "Montérégie", | |
| 180 | +"dunham": "Estrie", | |
| 181 | +"duparquet": "Abitibi-Témiscamingue", | |
| 182 | +"dupuy": "Abitibi-Témiscamingue", | |
| 183 | +"durham-sud": "Centre-du-Québec", | |
| 184 | +"east angus": "Estrie", | |
| 185 | +"east broughton": "Chaudière-Appalaches", | |
| 186 | +"east farnham": "Estrie", | |
| 187 | +"east hereford": "Estrie", | |
| 188 | +"eastmain": "Nord-du-Québec", | |
| 189 | +"eastman": "Estrie", | |
| 190 | +"eeyou istchee baie-james": "Nord-du-Québec", | |
| 191 | +"egan-sud": "Outaouais", | |
| 192 | +"elgin": "Montérégie", | |
| 193 | +"entrelacs": "Lanaudière", | |
| 194 | +"escuminac": "Gaspésie–Îles-de-la-Madeleine", | |
| 195 | +"esprit-saint": "Bas-Saint-Laurent", | |
| 196 | +"essipit": "Côte-Nord", | |
| 197 | +"esterel": "Laurentides", | |
| 198 | +"farnham": "Estrie", | |
| 199 | +"fassett": "Outaouais", | |
| 200 | +"ferland-et-boilleau": "Saguenay–Lac-Saint-Jean", | |
| 201 | +"ferme-neuve": "Laurentides", | |
| 202 | +"fermont": "Côte-Nord", | |
| 203 | +"forestville": "Côte-Nord", | |
| 204 | +"fort-coulonge": "Outaouais", | |
| 205 | +"fortierville": "Centre-du-Québec", | |
| 206 | +"fossambault-sur-le-lac": "Capitale-Nationale", | |
| 207 | +"frampton": "Chaudière-Appalaches", | |
| 208 | +"franklin": "Montérégie", | |
| 209 | +"franquelin": "Côte-Nord", | |
| 210 | +"frelighsburg": "Estrie", | |
| 211 | +"frontenac": "Estrie", | |
| 212 | +"fugereville": "Abitibi-Témiscamingue", | |
| 213 | +"gallichan": "Abitibi-Témiscamingue", | |
| 214 | +"gaspe": "Gaspésie–Îles-de-la-Madeleine", | |
| 215 | +"gatineau": "Outaouais", | |
| 216 | +"gesgapegiag": "Gaspésie–Îles-de-la-Madeleine", | |
| 217 | +"girardville": "Saguenay–Lac-Saint-Jean", | |
| 218 | +"godbout": "Côte-Nord", | |
| 219 | +"godmanchester": "Montérégie", | |
| 220 | +"gore": "Laurentides", | |
| 221 | +"gracefield": "Outaouais", | |
| 222 | +"granby": "Estrie", | |
| 223 | +"grand-metis": "Bas-Saint-Laurent", | |
| 224 | +"grand-remous": "Outaouais", | |
| 225 | +"grand-saint-esprit": "Centre-du-Québec", | |
| 226 | +"grande-riviere": "Gaspésie–Îles-de-la-Madeleine", | |
| 227 | +"grande-vallee": "Gaspésie–Îles-de-la-Madeleine", | |
| 228 | +"grandes-piles": "Mauricie", | |
| 229 | +"greenfield park": "Montérégie", | |
| 230 | +"grenville": "Laurentides", | |
| 231 | +"grenville-sur-la-rouge": "Laurentides", | |
| 232 | +"griffintown": "Montréal", | |
| 233 | +"gros-mecatina": "Côte-Nord", | |
| 234 | +"grosse-ile": "Gaspésie–Îles-de-la-Madeleine", | |
| 235 | +"grosses-roches": "Bas-Saint-Laurent", | |
| 236 | +"guerin": "Abitibi-Témiscamingue", | |
| 237 | +"ham-nord": "Centre-du-Québec", | |
| 238 | +"ham-sud": "Estrie", | |
| 239 | +"hampden": "Estrie", | |
| 240 | +"hampstead": "Montréal", | |
| 241 | +"harrington": "Laurentides", | |
| 242 | +"hatley": "Estrie", | |
| 243 | +"havelock": "Montérégie", | |
| 244 | +"havre-saint-pierre": "Côte-Nord", | |
| 245 | +"hebertville": "Saguenay–Lac-Saint-Jean", | |
| 246 | +"hemmingford": "Montérégie", | |
| 247 | +"henryville": "Montérégie", | |
| 248 | +"herouxville": "Mauricie", | |
| 249 | +"hinchinbrooke": "Montérégie", | |
| 250 | +"hochelaga-maisonneuve": "Montréal", | |
| 251 | +"honfleur": "Chaudière-Appalaches", | |
| 252 | +"hope": "Gaspésie–Îles-de-la-Madeleine", | |
| 253 | +"hope town": "Gaspésie–Îles-de-la-Madeleine", | |
| 254 | +"howick": "Montérégie", | |
| 255 | +"huberdeau": "Laurentides", | |
| 256 | +"hudson": "Montérégie", | |
| 257 | +"hull": "Outaouais", | |
| 258 | +"huntingdon": "Montérégie", | |
| 259 | +"ile-des-soeurs": "Montréal", | |
| 260 | +"inukjuak": "Nord-du-Québec", | |
| 261 | +"inverness": "Centre-du-Québec", | |
| 262 | +"irlande": "Chaudière-Appalaches", | |
| 263 | +"ivry-sur-le-lac": "Laurentides", | |
| 264 | +"ivujivik": "Nord-du-Québec", | |
| 265 | +"joliette": "Lanaudière", | |
| 266 | +"jonquiere": "Saguenay–Lac-Saint-Jean", | |
| 267 | +"kahnawake": "Montérégie", | |
| 268 | +"kamouraska": "Bas-Saint-Laurent", | |
| 269 | +"kangiqsualujjuaq": "Nord-du-Québec", | |
| 270 | +"kangiqsujuaq": "Nord-du-Québec", | |
| 271 | +"kangirsuk": "Nord-du-Québec", | |
| 272 | +"kataskomiq": "Bas-Saint-Laurent", | |
| 273 | +"kawawachikamach": "Nord-du-Québec", | |
| 274 | +"kazabazua": "Outaouais", | |
| 275 | +"kebaowek": "Abitibi-Témiscamingue", | |
| 276 | +"kiamika": "Laurentides", | |
| 277 | +"kingsbury": "Estrie", | |
| 278 | +"kingsey falls": "Centre-du-Québec", | |
| 279 | +"kinnear's mills": "Chaudière-Appalaches", | |
| 280 | +"kipawa": "Abitibi-Témiscamingue", | |
| 281 | +"kirkland": "Montréal", | |
| 282 | +"kitigan zibi": "Outaouais", | |
| 283 | +"kuujjuaq": "Nord-du-Québec", | |
| 284 | +"kuujjuarapik": "Nord-du-Québec", | |
| 285 | +"l'ancienne-lorette": "Capitale-Nationale", | |
| 286 | +"l'ange-gardien": "Outaouais", | |
| 287 | +"l'anse-saint-jean": "Saguenay–Lac-Saint-Jean", | |
| 288 | +"l'ascension": "Laurentides", | |
| 289 | +"l'ascension-de-notre-seigneur": "Saguenay–Lac-Saint-Jean", | |
| 290 | +"l'ascension-de-patapedia": "Gaspésie–Îles-de-la-Madeleine", | |
| 291 | +"l'assomption": "Lanaudière", | |
| 292 | +"l'avenir": "Centre-du-Québec", | |
| 293 | +"l'epiphanie": "Lanaudière", | |
| 294 | +"l'ile-cadieux": "Montérégie", | |
| 295 | +"l'ile-d'anticosti": "Côte-Nord", | |
| 296 | +"l'ile-dorval": "Montréal", | |
| 297 | +"l'ile-du-grand-calumet": "Outaouais", | |
| 298 | +"l'ile-perrot": "Montérégie", | |
| 299 | +"l'isle-aux-allumettes": "Outaouais", | |
| 300 | +"l'isle-aux-coudres": "Capitale-Nationale", | |
| 301 | +"l'isle-verte": "Bas-Saint-Laurent", | |
| 302 | +"l'islet": "Chaudière-Appalaches", | |
| 303 | +"la baie": "Saguenay–Lac-Saint-Jean", | |
| 304 | +"la bostonnais": "Mauricie", | |
| 305 | +"la conception": "Laurentides", | |
| 306 | +"la corne": "Abitibi-Témiscamingue", | |
| 307 | +"la dore": "Saguenay–Lac-Saint-Jean", | |
| 308 | +"la durantaye": "Chaudière-Appalaches", | |
| 309 | +"la guadeloupe": "Chaudière-Appalaches", | |
| 310 | +"la macaza": "Laurentides", | |
| 311 | +"la malbaie": "Capitale-Nationale", | |
| 312 | +"la martre": "Gaspésie–Îles-de-la-Madeleine", | |
| 313 | +"la minerve": "Laurentides", | |
| 314 | +"la morandiere-rochebaucourt": "Abitibi-Témiscamingue", | |
| 315 | +"la motte": "Abitibi-Témiscamingue", | |
| 316 | +"la patrie": "Estrie", | |
| 317 | +"la peche": "Outaouais", | |
| 318 | +"la pocatiere": "Bas-Saint-Laurent", | |
| 319 | +"la prairie": "Montérégie", | |
| 320 | +"la presentation": "Montérégie", | |
| 321 | +"la redemption": "Bas-Saint-Laurent", | |
| 322 | +"la reine": "Abitibi-Témiscamingue", | |
| 323 | +"la romaine": "Côte-Nord", | |
| 324 | +"la sarre": "Abitibi-Témiscamingue", | |
| 325 | +"la trinite-des-monts": "Bas-Saint-Laurent", | |
| 326 | +"la tuque": "Mauricie", | |
| 327 | +"la visitation-de-l'ile-dupas": "Lanaudière", | |
| 328 | +"la visitation-de-yamaska": "Centre-du-Québec", | |
| 329 | +"labelle": "Laurentides", | |
| 330 | +"labrecque": "Saguenay–Lac-Saint-Jean", | |
| 331 | +"lac-a-la-croix": "Bas-Saint-Laurent", | |
| 332 | +"lac-achouakan": "Saguenay–Lac-Saint-Jean", | |
| 333 | +"lac-akonapwehikan": "Laurentides", | |
| 334 | +"lac-alfred": "Bas-Saint-Laurent", | |
| 335 | +"lac-ashuapmushuan": "Saguenay–Lac-Saint-Jean", | |
| 336 | +"lac-au-brochet": "Côte-Nord", | |
| 337 | +"lac-au-saumon": "Bas-Saint-Laurent", | |
| 338 | +"lac-aux-sables": "Mauricie", | |
| 339 | +"lac-bazinet": "Laurentides", | |
| 340 | +"lac-beauport": "Capitale-Nationale", | |
| 341 | +"lac-blanc": "Capitale-Nationale", | |
| 342 | +"lac-boisbouscache": "Bas-Saint-Laurent", | |
| 343 | +"lac-bouchette": "Saguenay–Lac-Saint-Jean", | |
| 344 | +"lac-boule": "Mauricie", | |
| 345 | +"lac-brome": "Estrie", | |
| 346 | +"lac-cabasta": "Lanaudière", | |
| 347 | +"lac-casault": "Bas-Saint-Laurent", | |
| 348 | +"lac-chicobi": "Abitibi-Témiscamingue", | |
| 349 | +"lac-croche": "Capitale-Nationale", | |
| 350 | +"lac-de la bidiere": "Laurentides", | |
| 351 | +"lac-de-la-maison-de-pierre": "Laurentides", | |
| 352 | +"lac-de-la-pomme": "Laurentides", | |
| 353 | +"lac-delage": "Capitale-Nationale", | |
| 354 | +"lac-des-aigles": "Bas-Saint-Laurent", | |
| 355 | +"lac-des-dix-milles": "Lanaudière", | |
| 356 | +"lac-des-eaux-mortes": "Bas-Saint-Laurent", | |
| 357 | +"lac-des-ecorces": "Laurentides", | |
| 358 | +"lac-des-plages": "Outaouais", | |
| 359 | +"lac-des-seize-iles": "Laurentides", | |
| 360 | +"lac-despinassy": "Abitibi-Témiscamingue", | |
| 361 | +"lac-devenyns": "Lanaudière", | |
| 362 | +"lac-douaire": "Laurentides", | |
| 363 | +"lac-drolet": "Estrie", | |
| 364 | +"lac-du-cerf": "Laurentides", | |
| 365 | +"lac-du-taureau": "Lanaudière", | |
| 366 | +"lac-duparquet": "Abitibi-Témiscamingue", | |
| 367 | +"lac-edouard": "Mauricie", | |
| 368 | +"lac-ernest": "Laurentides", | |
| 369 | +"lac-etchemin": "Chaudière-Appalaches", | |
| 370 | +"lac-frontiere": "Chaudière-Appalaches", | |
| 371 | +"lac-granet": "Abitibi-Témiscamingue", | |
| 372 | +"lac-huron": "Bas-Saint-Laurent", | |
| 373 | +"lac-jacques-cartier": "Capitale-Nationale", | |
| 374 | +"lac-jerome": "Côte-Nord", | |
| 375 | +"lac-john": "Côte-Nord", | |
| 376 | +"lac-juillet": "Côte-Nord", | |
| 377 | +"lac-lapeyrere": "Capitale-Nationale", | |
| 378 | +"lac-legendre": "Lanaudière", | |
| 379 | +"lac-lenotre": "Outaouais", | |
| 380 | +"lac-marguerite": "Laurentides", | |
| 381 | +"lac-masketsi": "Mauricie", | |
| 382 | +"lac-matapedia": "Bas-Saint-Laurent", | |
| 383 | +"lac-matawin": "Lanaudière", | |
| 384 | +"lac-megantic": "Estrie", | |
| 385 | +"lac-metei": "Abitibi-Témiscamingue", | |
| 386 | +"lac-minaki": "Lanaudière", | |
| 387 | +"lac-ministuk": "Saguenay–Lac-Saint-Jean", | |
| 388 | +"lac-moncouche": "Saguenay–Lac-Saint-Jean", | |
| 389 | +"lac-moselle": "Outaouais", | |
| 390 | +"lac-nilgaut": "Outaouais", | |
| 391 | +"lac-normand": "Mauricie", | |
| 392 | +"lac-oscar": "Laurentides", | |
| 393 | +"lac-pikauba": "Capitale-Nationale", | |
| 394 | +"lac-poulin": "Chaudière-Appalaches", | |
| 395 | +"lac-pythonga": "Outaouais", | |
| 396 | +"lac-rapide": "Outaouais", | |
| 397 | +"lac-saguay": "Laurentides", | |
| 398 | +"lac-saint-joseph": "Capitale-Nationale", | |
| 399 | +"lac-saint-paul": "Laurentides", | |
| 400 | +"lac-sainte-marie": "Outaouais", | |
| 401 | +"lac-sante": "Lanaudière", | |
| 402 | +"lac-sergent": "Capitale-Nationale", | |
| 403 | +"lac-simon": "Abitibi-Témiscamingue", | |
| 404 | +"lac-superieur": "Laurentides", | |
| 405 | +"lac-tremblant-nord": "Laurentides", | |
| 406 | +"lac-vacher": "Côte-Nord", | |
| 407 | +"lac-wagwabika": "Laurentides", | |
| 408 | +"lac-walker": "Côte-Nord", | |
| 409 | +"lachine": "Montréal", | |
| 410 | +"lachute": "Laurentides", | |
| 411 | +"lacolle": "Montérégie", | |
| 412 | +"laforce": "Abitibi-Témiscamingue", | |
| 413 | +"lalemant": "Saguenay–Lac-Saint-Jean", | |
| 414 | +"lamarche": "Saguenay–Lac-Saint-Jean", | |
| 415 | +"lambton": "Estrie", | |
| 416 | +"landrienne": "Abitibi-Témiscamingue", | |
| 417 | +"laniel": "Abitibi-Témiscamingue", | |
| 418 | +"lanoraie": "Lanaudière", | |
| 419 | +"lantier": "Laurentides", | |
| 420 | +"larouche": "Saguenay–Lac-Saint-Jean", | |
| 421 | +"lasalle": "Montréal", | |
| 422 | +"latulipe-et-gaboury": "Abitibi-Témiscamingue", | |
| 423 | +"launay": "Abitibi-Témiscamingue", | |
| 424 | +"laurier-station": "Chaudière-Appalaches", | |
| 425 | +"laurierville": "Centre-du-Québec", | |
| 426 | +"laval": "Laval", | |
| 427 | +"lavaltrie": "Lanaudière", | |
| 428 | +"laverlochere-angliers": "Abitibi-Témiscamingue", | |
| 429 | +"lawrenceville": "Estrie", | |
| 430 | +"le plateau-mont-royal": "Montréal", | |
| 431 | +"lebel-sur-quevillon": "Nord-du-Québec", | |
| 432 | +"leclercville": "Chaudière-Appalaches", | |
| 433 | +"lefebvre": "Centre-du-Québec", | |
| 434 | +"lejeune": "Bas-Saint-Laurent", | |
| 435 | +"lemieux": "Centre-du-Québec", | |
| 436 | +"lery": "Montérégie", | |
| 437 | +"les bergeronnes": "Côte-Nord", | |
| 438 | +"les cedres": "Montérégie", | |
| 439 | +"les coteaux": "Montérégie", | |
| 440 | +"les eboulements": "Capitale-Nationale", | |
| 441 | +"les escoumins": "Côte-Nord", | |
| 442 | +"les hauteurs": "Bas-Saint-Laurent", | |
| 443 | +"les iles-de-la-madeleine": "Gaspésie–Îles-de-la-Madeleine", | |
| 444 | +"les lacs-du-temiscamingue": "Abitibi-Témiscamingue", | |
| 445 | +"les mechins": "Bas-Saint-Laurent", | |
| 446 | +"levis": "Chaudière-Appalaches", | |
| 447 | +"lingwick": "Estrie", | |
| 448 | +"linton": "Capitale-Nationale", | |
| 449 | +"listuguj": "Gaspésie–Îles-de-la-Madeleine", | |
| 450 | +"litchfield": "Outaouais", | |
| 451 | +"lochaber": "Outaouais", | |
| 452 | +"lochaber-partie-ouest": "Outaouais", | |
| 453 | +"longue-pointe-de-mingan": "Côte-Nord", | |
| 454 | +"longue-rive": "Côte-Nord", | |
| 455 | +"longueuil": "Montérégie", | |
| 456 | +"loretteville": "Capitale-Nationale", | |
| 457 | +"lorraine": "Laurentides", | |
| 458 | +"lorrainville": "Abitibi-Témiscamingue", | |
| 459 | +"lotbiniere": "Chaudière-Appalaches", | |
| 460 | +"louiseville": "Mauricie", | |
| 461 | +"low": "Outaouais", | |
| 462 | +"lyster": "Centre-du-Québec", | |
| 463 | +"macamic": "Abitibi-Témiscamingue", | |
| 464 | +"maddington falls": "Centre-du-Québec", | |
| 465 | +"magog": "Estrie", | |
| 466 | +"malartic": "Abitibi-Témiscamingue", | |
| 467 | +"maliotenam": "Côte-Nord", | |
| 468 | +"manawan": "Lanaudière", | |
| 469 | +"mandeville": "Lanaudière", | |
| 470 | +"maniwaki": "Outaouais", | |
| 471 | +"manseau": "Centre-du-Québec", | |
| 472 | +"mansfield-et-pontefract": "Outaouais", | |
| 473 | +"maria": "Gaspésie–Îles-de-la-Madeleine", | |
| 474 | +"maricourt": "Estrie", | |
| 475 | +"marieville": "Montérégie", | |
| 476 | +"marsoui": "Gaspésie–Îles-de-la-Madeleine", | |
| 477 | +"marston": "Estrie", | |
| 478 | +"martinville": "Estrie", | |
| 479 | +"mascouche": "Lanaudière", | |
| 480 | +"mashteuiatsh": "Saguenay–Lac-Saint-Jean", | |
| 481 | +"maskinonge": "Mauricie", | |
| 482 | +"massueville": "Montérégie", | |
| 483 | +"matagami": "Nord-du-Québec", | |
| 484 | +"matane": "Bas-Saint-Laurent", | |
| 485 | +"matapedia": "Gaspésie–Îles-de-la-Madeleine", | |
| 486 | +"matchi-manitou": "Abitibi-Témiscamingue", | |
| 487 | +"matimekosh": "Côte-Nord", | |
| 488 | +"mayo": "Outaouais", | |
| 489 | +"mcmasterville": "Montérégie", | |
| 490 | +"melbourne": "Estrie", | |
| 491 | +"mercier": "Montérégie", | |
| 492 | +"messines": "Outaouais", | |
| 493 | +"metabetchouan--lac-a-la-croix": "Saguenay–Lac-Saint-Jean", | |
| 494 | +"metis-sur-mer": "Bas-Saint-Laurent", | |
| 495 | +"milan": "Estrie", | |
| 496 | +"mille-isles": "Laurentides", | |
| 497 | +"mingan": "Côte-Nord", | |
| 498 | +"mirabel": "Laurentides", | |
| 499 | +"mistissini": "Nord-du-Québec", | |
| 500 | +"moffet": "Abitibi-Témiscamingue", | |
| 501 | +"mont-albert": "Gaspésie–Îles-de-la-Madeleine", | |
| 502 | +"mont-alexandre": "Gaspésie–Îles-de-la-Madeleine", | |
| 503 | +"mont-apica": "Saguenay–Lac-Saint-Jean", | |
| 504 | +"mont-blanc": "Laurentides", | |
| 505 | +"mont-carmel": "Bas-Saint-Laurent", | |
| 506 | +"mont-elie": "Capitale-Nationale", | |
| 507 | +"mont-joli": "Bas-Saint-Laurent", | |
| 508 | +"mont-laurier": "Laurentides", | |
| 509 | +"mont-royal": "Montréal", | |
| 510 | +"mont-saint-gregoire": "Montérégie", | |
| 511 | +"mont-saint-hilaire": "Montérégie", | |
| 512 | +"mont-saint-michel": "Laurentides", | |
| 513 | +"mont-saint-pierre": "Gaspésie–Îles-de-la-Madeleine", | |
| 514 | +"mont-tremblant": "Laurentides", | |
| 515 | +"mont-valin": "Saguenay–Lac-Saint-Jean", | |
| 516 | +"montcalm": "Laurentides", | |
| 517 | +"montcerf-lytton": "Outaouais", | |
| 518 | +"montebello": "Outaouais", | |
| 519 | +"montmagny": "Chaudière-Appalaches", | |
| 520 | +"montpellier": "Outaouais", | |
| 521 | +"montreal": "Montréal", | |
| 522 | +"montreal-est": "Montréal", | |
| 523 | +"montreal-nord": "Montréal", | |
| 524 | +"montreal-ouest": "Montréal", | |
| 525 | +"morin-heights": "Laurentides", | |
| 526 | +"mulgrave-et-derry": "Outaouais", | |
| 527 | +"murdochville": "Gaspésie–Îles-de-la-Madeleine", | |
| 528 | +"namur": "Outaouais", | |
| 529 | +"nantes": "Estrie", | |
| 530 | +"napierville": "Montérégie", | |
| 531 | +"natashquan": "Côte-Nord", | |
| 532 | +"nedelec": "Abitibi-Témiscamingue", | |
| 533 | +"nemaska": "Nord-du-Québec", | |
| 534 | +"neuville": "Capitale-Nationale", | |
| 535 | +"new carlisle": "Gaspésie–Îles-de-la-Madeleine", | |
| 536 | +"new richmond": "Gaspésie–Îles-de-la-Madeleine", | |
| 537 | +"newport": "Estrie", | |
| 538 | +"nicolet": "Centre-du-Québec", | |
| 539 | +"nominingue": "Laurentides", | |
| 540 | +"normandin": "Saguenay–Lac-Saint-Jean", | |
| 541 | +"normetal": "Abitibi-Témiscamingue", | |
| 542 | +"north hatley": "Estrie", | |
| 543 | +"notre-dame-auxiliatrice-de-buckland": "Chaudière-Appalaches", | |
| 544 | +"notre-dame-de-bonsecours": "Outaouais", | |
| 545 | +"notre-dame-de-ham": "Centre-du-Québec", | |
| 546 | +"notre-dame-de-l'ile-perrot": "Montérégie", | |
| 547 | +"notre-dame-de-la-merci": "Lanaudière", | |
| 548 | +"notre-dame-de-la-paix": "Outaouais", | |
| 549 | +"notre-dame-de-la-salette": "Outaouais", | |
| 550 | +"notre-dame-de-lorette": "Saguenay–Lac-Saint-Jean", | |
| 551 | +"notre-dame-de-lourdes": "Centre-du-Québec", | |
| 552 | +"notre-dame-de-montauban": "Mauricie", | |
| 553 | +"notre-dame-de-pontmain": "Laurentides", | |
| 554 | +"notre-dame-de-stanbridge": "Estrie", | |
| 555 | +"notre-dame-des-anges": "Capitale-Nationale", | |
| 556 | +"notre-dame-des-bois": "Estrie", | |
| 557 | +"notre-dame-des-monts": "Capitale-Nationale", | |
| 558 | +"notre-dame-des-neiges": "Bas-Saint-Laurent", | |
| 559 | +"notre-dame-des-pins": "Chaudière-Appalaches", | |
| 560 | +"notre-dame-des-prairies": "Lanaudière", | |
| 561 | +"notre-dame-des-sept-douleurs": "Bas-Saint-Laurent", | |
| 562 | +"notre-dame-du-bon-conseil": "Centre-du-Québec", | |
| 563 | +"notre-dame-du-laus": "Laurentides", | |
| 564 | +"notre-dame-du-mont-carmel": "Mauricie", | |
| 565 | +"notre-dame-du-nord": "Abitibi-Témiscamingue", | |
| 566 | +"notre-dame-du-portage": "Bas-Saint-Laurent", | |
| 567 | +"notre-dame-du-rosaire": "Chaudière-Appalaches", | |
| 568 | +"notre-dame-du-sacre-coeur-d'issoudun": "Chaudière-Appalaches", | |
| 569 | +"nouvelle": "Gaspésie–Îles-de-la-Madeleine", | |
| 570 | +"noyan": "Montérégie", | |
| 571 | +"obedjiwan": "Mauricie", | |
| 572 | +"odanak": "Centre-du-Québec", | |
| 573 | +"ogden": "Estrie", | |
| 574 | +"oka": "Laurentides", | |
| 575 | +"orford": "Estrie", | |
| 576 | +"ormstown": "Montérégie", | |
| 577 | +"otter lake": "Outaouais", | |
| 578 | +"otterburn park": "Montérégie", | |
| 579 | +"ouje-bougoumou": "Nord-du-Québec", | |
| 580 | +"outremont": "Montréal", | |
| 581 | +"packington": "Bas-Saint-Laurent", | |
| 582 | +"padoue": "Bas-Saint-Laurent", | |
| 583 | +"palmarolle": "Abitibi-Témiscamingue", | |
| 584 | +"papineauville": "Outaouais", | |
| 585 | +"parisville": "Centre-du-Québec", | |
| 586 | +"paspebiac": "Gaspésie–Îles-de-la-Madeleine", | |
| 587 | +"passes-dangereuses": "Saguenay–Lac-Saint-Jean", | |
| 588 | +"perce": "Gaspésie–Îles-de-la-Madeleine", | |
| 589 | +"peribonka": "Saguenay–Lac-Saint-Jean", | |
| 590 | +"pessamit": "Côte-Nord", | |
| 591 | +"petit-lac-sainte-anne": "Bas-Saint-Laurent", | |
| 592 | +"petit-mecatina": "Côte-Nord", | |
| 593 | +"petit-saguenay": "Saguenay–Lac-Saint-Jean", | |
| 594 | +"petite-riviere-saint-francois": "Capitale-Nationale", | |
| 595 | +"petite-vallee": "Gaspésie–Îles-de-la-Madeleine", | |
| 596 | +"picard": "Bas-Saint-Laurent", | |
| 597 | +"piedmont": "Laurentides", | |
| 598 | +"pierrefonds": "Montréal", | |
| 599 | +"pierreville": "Centre-du-Québec", | |
| 600 | +"pike river": "Estrie", | |
| 601 | +"pikogan": "Abitibi-Témiscamingue", | |
| 602 | +"pincourt": "Montérégie", | |
| 603 | +"piopolis": "Estrie", | |
| 604 | +"plaisance": "Outaouais", | |
| 605 | +"plessisville": "Centre-du-Québec", | |
| 606 | +"pohenegamook": "Bas-Saint-Laurent", | |
| 607 | +"pointe-a-la-croix": "Gaspésie–Îles-de-la-Madeleine", | |
| 608 | +"pointe-aux-outardes": "Côte-Nord", | |
| 609 | +"pointe-aux-trembles": "Montréal", | |
| 610 | +"pointe-calumet": "Laurentides", | |
| 611 | +"pointe-claire": "Montréal", | |
| 612 | +"pointe-des-cascades": "Montérégie", | |
| 613 | +"pointe-fortune": "Montérégie", | |
| 614 | +"pointe-lebel": "Côte-Nord", | |
| 615 | +"pont-rouge": "Capitale-Nationale", | |
| 616 | +"pontiac": "Outaouais", | |
| 617 | +"port-cartier": "Côte-Nord", | |
| 618 | +"port-daniel--gascons": "Gaspésie–Îles-de-la-Madeleine", | |
| 619 | +"portage-du-fort": "Outaouais", | |
| 620 | +"portneuf": "Capitale-Nationale", | |
| 621 | +"portneuf-sur-mer": "Côte-Nord", | |
| 622 | +"potton": "Estrie", | |
| 623 | +"poularies": "Abitibi-Témiscamingue", | |
| 624 | +"preissac": "Abitibi-Témiscamingue", | |
| 625 | +"prevost": "Laurentides", | |
| 626 | +"price": "Bas-Saint-Laurent", | |
| 627 | +"princeville": "Centre-du-Québec", | |
| 628 | +"puvirnituq": "Nord-du-Québec", | |
| 629 | +"quaqtaq": "Nord-du-Québec", | |
| 630 | +"quebec": "Capitale-Nationale", | |
| 631 | +"quebec city": "Capitale-Nationale", | |
| 632 | +"racine": "Estrie", | |
| 633 | +"ragueneau": "Côte-Nord", | |
| 634 | +"rapide-danseur": "Abitibi-Témiscamingue", | |
| 635 | +"rapides-des-joachims": "Outaouais", | |
| 636 | +"rawdon": "Lanaudière", | |
| 637 | +"remigny": "Abitibi-Témiscamingue", | |
| 638 | +"repentigny": "Lanaudière", | |
| 639 | +"reservoir-dozois": "Abitibi-Témiscamingue", | |
| 640 | +"richelieu": "Montérégie", | |
| 641 | +"richmond": "Estrie", | |
| 642 | +"rigaud": "Montérégie", | |
| 643 | +"rimouski": "Bas-Saint-Laurent", | |
| 644 | +"ripon": "Outaouais", | |
| 645 | +"ristigouche-sud-est": "Gaspésie–Îles-de-la-Madeleine", | |
| 646 | +"riviere-a-claude": "Gaspésie–Îles-de-la-Madeleine", | |
| 647 | +"riviere-a-pierre": "Capitale-Nationale", | |
| 648 | +"riviere-au-tonnerre": "Côte-Nord", | |
| 649 | +"riviere-aux-outardes": "Côte-Nord", | |
| 650 | +"riviere-beaudette": "Montérégie", | |
| 651 | +"riviere-bleue": "Bas-Saint-Laurent", | |
| 652 | +"riviere-bonaventure": "Gaspésie–Îles-de-la-Madeleine", | |
| 653 | +"riviere-bonjour": "Bas-Saint-Laurent", | |
| 654 | +"riviere-de-la-savane": "Mauricie", | |
| 655 | +"riviere-des-prairies": "Montréal", | |
| 656 | +"riviere-du-loup": "Bas-Saint-Laurent", | |
| 657 | +"riviere-eternite": "Saguenay–Lac-Saint-Jean", | |
| 658 | +"riviere-heva": "Abitibi-Témiscamingue", | |
| 659 | +"riviere-koksoak": "Nord-du-Québec", | |
| 660 | +"riviere-mistassini": "Saguenay–Lac-Saint-Jean", | |
| 661 | +"riviere-mouchalagane": "Côte-Nord", | |
| 662 | +"riviere-nipissis": "Côte-Nord", | |
| 663 | +"riviere-nouvelle": "Gaspésie–Îles-de-la-Madeleine", | |
| 664 | +"riviere-ojima": "Abitibi-Témiscamingue", | |
| 665 | +"riviere-ouelle": "Bas-Saint-Laurent", | |
| 666 | +"riviere-patapedia-est": "Bas-Saint-Laurent", | |
| 667 | +"riviere-rouge": "Laurentides", | |
| 668 | +"riviere-saint-jean": "Gaspésie–Îles-de-la-Madeleine", | |
| 669 | +"riviere-vaseuse": "Bas-Saint-Laurent", | |
| 670 | +"roberval": "Saguenay–Lac-Saint-Jean", | |
| 671 | +"roquemaure": "Abitibi-Témiscamingue", | |
| 672 | +"rosemere": "Laurentides", | |
| 673 | +"rosemont": "Montréal", | |
| 674 | +"rougemont": "Montérégie", | |
| 675 | +"routhierville": "Bas-Saint-Laurent", | |
| 676 | +"rouyn-noranda": "Abitibi-Témiscamingue", | |
| 677 | +"roxboro": "Montréal", | |
| 678 | +"roxton": "Montérégie", | |
| 679 | +"roxton falls": "Montérégie", | |
| 680 | +"roxton pond": "Estrie", | |
| 681 | +"ruisseau-des-mineurs": "Bas-Saint-Laurent", | |
| 682 | +"ruisseau-ferguson": "Gaspésie–Îles-de-la-Madeleine", | |
| 683 | +"sacre-coeur": "Côte-Nord", | |
| 684 | +"sacre-coeur-de-jesus": "Chaudière-Appalaches", | |
| 685 | +"sagard": "Capitale-Nationale", | |
| 686 | +"saguenay": "Saguenay–Lac-Saint-Jean", | |
| 687 | +"saint-adalbert": "Chaudière-Appalaches", | |
| 688 | +"saint-adelme": "Bas-Saint-Laurent", | |
| 689 | +"saint-adelphe": "Mauricie", | |
| 690 | +"saint-adolphe-d'howard": "Laurentides", | |
| 691 | +"saint-adrien": "Estrie", | |
| 692 | +"saint-adrien-d'irlande": "Chaudière-Appalaches", | |
| 693 | +"saint-agapit": "Chaudière-Appalaches", | |
| 694 | +"saint-aime": "Montérégie", | |
| 695 | +"saint-aime-des-lacs": "Capitale-Nationale", | |
| 696 | +"saint-aime-du-lac-des-iles": "Laurentides", | |
| 697 | +"saint-alban": "Capitale-Nationale", | |
| 698 | +"saint-albert": "Centre-du-Québec", | |
| 699 | +"saint-alexandre": "Montérégie", | |
| 700 | +"saint-alexandre-de-kamouraska": "Bas-Saint-Laurent", | |
| 701 | +"saint-alexandre-des-lacs": "Bas-Saint-Laurent", | |
| 702 | +"saint-alexis": "Lanaudière", | |
| 703 | +"saint-alexis-de-matapedia": "Gaspésie–Îles-de-la-Madeleine", | |
| 704 | +"saint-alexis-des-monts": "Mauricie", | |
| 705 | +"saint-alfred": "Chaudière-Appalaches", | |
| 706 | +"saint-alphonse": "Gaspésie–Îles-de-la-Madeleine", | |
| 707 | +"saint-alphonse-de-granby": "Estrie", | |
| 708 | +"saint-alphonse-rodriguez": "Lanaudière", | |
| 709 | +"saint-amable": "Montérégie", | |
| 710 | +"saint-ambroise": "Saguenay–Lac-Saint-Jean", | |
| 711 | +"saint-ambroise-de-kildare": "Lanaudière", | |
| 712 | +"saint-anaclet-de-lessard": "Bas-Saint-Laurent", | |
| 713 | +"saint-andre-avellin": "Outaouais", | |
| 714 | +"saint-andre-d'argenteuil": "Laurentides", | |
| 715 | +"saint-andre-de-kamouraska": "Bas-Saint-Laurent", | |
| 716 | +"saint-andre-de-restigouche": "Gaspésie–Îles-de-la-Madeleine", | |
| 717 | +"saint-andre-du-lac-saint-jean": "Saguenay–Lac-Saint-Jean", | |
| 718 | +"saint-anicet": "Montérégie", | |
| 719 | +"saint-anselme": "Chaudière-Appalaches", | |
| 720 | +"saint-antoine-de-l'isle-aux-grues": "Chaudière-Appalaches", | |
| 721 | +"saint-antoine-de-tilly": "Chaudière-Appalaches", | |
| 722 | +"saint-antoine-sur-richelieu": "Montérégie", | |
| 723 | +"saint-antonin": "Bas-Saint-Laurent", | |
| 724 | +"saint-apollinaire": "Chaudière-Appalaches", | |
| 725 | +"saint-armand": "Estrie", | |
| 726 | +"saint-arsene": "Bas-Saint-Laurent", | |
| 727 | +"saint-athanase": "Bas-Saint-Laurent", | |
| 728 | +"saint-aubert": "Chaudière-Appalaches", | |
| 729 | +"saint-augustin": "Saguenay–Lac-Saint-Jean", | |
| 730 | +"saint-augustin-de-desmaures": "Capitale-Nationale", | |
| 731 | +"saint-augustin-de-woburn": "Estrie", | |
| 732 | +"saint-barnabe": "Mauricie", | |
| 733 | +"saint-barnabe-sud": "Montérégie", | |
| 734 | +"saint-barthelemy": "Lanaudière", | |
| 735 | +"saint-basile": "Capitale-Nationale", | |
| 736 | +"saint-basile-le-grand": "Montérégie", | |
| 737 | +"saint-benjamin": "Chaudière-Appalaches", | |
| 738 | +"saint-benoit-du-lac": "Estrie", | |
| 739 | +"saint-benoit-labre": "Chaudière-Appalaches", | |
| 740 | +"saint-bernard": "Chaudière-Appalaches", | |
| 741 | +"saint-bernard-de-lacolle": "Montérégie", | |
| 742 | +"saint-bernard-de-michaudville": "Montérégie", | |
| 743 | +"saint-blaise-sur-richelieu": "Montérégie", | |
| 744 | +"saint-bonaventure": "Centre-du-Québec", | |
| 745 | +"saint-boniface": "Mauricie", | |
| 746 | +"saint-bruno-de-guigues": "Abitibi-Témiscamingue", | |
| 747 | +"saint-bruno-de-kamouraska": "Bas-Saint-Laurent", | |
| 748 | +"saint-bruno-de-montarville": "Montérégie", | |
| 749 | +"saint-calixte": "Lanaudière", | |
| 750 | +"saint-camille": "Estrie", | |
| 751 | +"saint-camille-de-lellis": "Chaudière-Appalaches", | |
| 752 | +"saint-casimir": "Capitale-Nationale", | |
| 753 | +"saint-celestin": "Centre-du-Québec", | |
| 754 | +"saint-cesaire": "Montérégie", | |
| 755 | +"saint-charles-borromee": "Lanaudière", | |
| 756 | +"saint-charles-de-bellechasse": "Chaudière-Appalaches", | |
| 757 | +"saint-charles-de-bourget": "Saguenay–Lac-Saint-Jean", | |
| 758 | +"saint-charles-garnier": "Bas-Saint-Laurent", | |
| 759 | +"saint-charles-sur-richelieu": "Montérégie", | |
| 760 | +"saint-christophe-d'arthabaska": "Centre-du-Québec", | |
| 761 | +"saint-chrysostome": "Montérégie", | |
| 762 | +"saint-claude": "Estrie", | |
| 763 | +"saint-clement": "Bas-Saint-Laurent", | |
| 764 | +"saint-cleophas": "Bas-Saint-Laurent", | |
| 765 | +"saint-cleophas-de-brandon": "Lanaudière", | |
| 766 | +"saint-clet": "Montérégie", | |
| 767 | +"saint-colomban": "Laurentides", | |
| 768 | +"saint-come": "Lanaudière", | |
| 769 | +"saint-come--liniere": "Chaudière-Appalaches", | |
| 770 | +"saint-constant": "Montérégie", | |
| 771 | +"saint-cuthbert": "Lanaudière", | |
| 772 | +"saint-cyprien": "Chaudière-Appalaches", | |
| 773 | +"saint-cyprien-de-napierville": "Montérégie", | |
| 774 | +"saint-cyrille-de-lessard": "Chaudière-Appalaches", | |
| 775 | +"saint-cyrille-de-wendover": "Centre-du-Québec", | |
| 776 | +"saint-damase": "Montérégie", | |
| 777 | +"saint-damase-de-l'islet": "Chaudière-Appalaches", | |
| 778 | +"saint-damien": "Lanaudière", | |
| 779 | +"saint-damien-de-buckland": "Chaudière-Appalaches", | |
| 780 | +"saint-david": "Montérégie", | |
| 781 | +"saint-david-de-falardeau": "Saguenay–Lac-Saint-Jean", | |
| 782 | +"saint-denis-de la bouteillerie": "Bas-Saint-Laurent", | |
| 783 | +"saint-denis-de-brompton": "Estrie", | |
| 784 | +"saint-denis-sur-richelieu": "Montérégie", | |
| 785 | +"saint-didace": "Lanaudière", | |
| 786 | +"saint-dominique": "Montérégie", | |
| 787 | +"saint-dominique-du-rosaire": "Abitibi-Témiscamingue", | |
| 788 | +"saint-donat": "Bas-Saint-Laurent", | |
| 789 | +"saint-edmond-de-grantham": "Centre-du-Québec", | |
| 790 | +"saint-edmond-les-plaines": "Saguenay–Lac-Saint-Jean", | |
| 791 | +"saint-edouard": "Montérégie", | |
| 792 | +"saint-edouard-de-fabre": "Abitibi-Témiscamingue", | |
| 793 | +"saint-edouard-de-lotbiniere": "Chaudière-Appalaches", | |
| 794 | +"saint-edouard-de-maskinonge": "Mauricie", | |
| 795 | +"saint-elie-de-caxton": "Mauricie", | |
| 796 | +"saint-eloi": "Bas-Saint-Laurent", | |
| 797 | +"saint-elphege": "Centre-du-Québec", | |
| 798 | +"saint-elzear": "Chaudière-Appalaches", | |
| 799 | +"saint-elzear-de-temiscouata": "Bas-Saint-Laurent", | |
| 800 | +"saint-emile-de-suffolk": "Outaouais", | |
| 801 | +"saint-ephrem-de-beauce": "Chaudière-Appalaches", | |
| 802 | +"saint-epiphane": "Bas-Saint-Laurent", | |
| 803 | +"saint-esprit": "Lanaudière", | |
| 804 | +"saint-etienne-de-beauharnois": "Montérégie", | |
| 805 | +"saint-etienne-de-bolton": "Estrie", | |
| 806 | +"saint-etienne-des-gres": "Mauricie", | |
| 807 | +"saint-eugene": "Centre-du-Québec", | |
| 808 | +"saint-eugene-d'argentenay": "Saguenay–Lac-Saint-Jean", | |
| 809 | +"saint-eugene-de-guigues": "Abitibi-Témiscamingue", | |
| 810 | +"saint-eugene-de-ladriere": "Bas-Saint-Laurent", | |
| 811 | +"saint-eusebe": "Bas-Saint-Laurent", | |
| 812 | +"saint-eustache": "Laurentides", | |
| 813 | +"saint-fabien": "Bas-Saint-Laurent", | |
| 814 | +"saint-fabien-de-panet": "Chaudière-Appalaches", | |
| 815 | +"saint-felicien": "Saguenay–Lac-Saint-Jean", | |
| 816 | +"saint-felix-d'otis": "Saguenay–Lac-Saint-Jean", | |
| 817 | +"saint-felix-de-kingsey": "Centre-du-Québec", | |
| 818 | +"saint-felix-de-valois": "Lanaudière", | |
| 819 | +"saint-ferdinand": "Centre-du-Québec", | |
| 820 | +"saint-ferreol-les-neiges": "Capitale-Nationale", | |
| 821 | +"saint-flavien": "Chaudière-Appalaches", | |
| 822 | +"saint-fortunat": "Chaudière-Appalaches", | |
| 823 | +"saint-francois-d'assise": "Gaspésie–Îles-de-la-Madeleine", | |
| 824 | +"saint-francois-de-l'ile-d'orleans": "Capitale-Nationale", | |
| 825 | +"saint-francois-de-la-riviere-du-sud": "Chaudière-Appalaches", | |
| 826 | +"saint-francois-de-sales": "Saguenay–Lac-Saint-Jean", | |
| 827 | +"saint-francois-du-lac": "Centre-du-Québec", | |
| 828 | +"saint-francois-xavier-de-brompton": "Estrie", | |
| 829 | +"saint-francois-xavier-de-viger": "Bas-Saint-Laurent", | |
| 830 | +"saint-frederic": "Chaudière-Appalaches", | |
| 831 | +"saint-fulgence": "Saguenay–Lac-Saint-Jean", | |
| 832 | +"saint-gabriel": "Lanaudière", | |
| 833 | +"saint-gabriel-de-brandon": "Lanaudière", | |
| 834 | +"saint-gabriel-de-rimouski": "Bas-Saint-Laurent", | |
| 835 | +"saint-gabriel-de-valcartier": "Capitale-Nationale", | |
| 836 | +"saint-gabriel-lalemant": "Bas-Saint-Laurent", | |
| 837 | +"saint-gedeon": "Saguenay–Lac-Saint-Jean", | |
| 838 | +"saint-gedeon-de-beauce": "Chaudière-Appalaches", | |
| 839 | +"saint-georges": "Chaudière-Appalaches", | |
| 840 | +"saint-georges-de-windsor": "Estrie", | |
| 841 | +"saint-gerard-majella": "Montérégie", | |
| 842 | +"saint-germain-de-grantham": "Centre-du-Québec", | |
| 843 | +"saint-germain-de-kamouraska": "Bas-Saint-Laurent", | |
| 844 | +"saint-gervais": "Chaudière-Appalaches", | |
| 845 | +"saint-gilbert": "Capitale-Nationale", | |
| 846 | +"saint-gilles": "Chaudière-Appalaches", | |
| 847 | +"saint-godefroi": "Gaspésie–Îles-de-la-Madeleine", | |
| 848 | +"saint-guillaume": "Centre-du-Québec", | |
| 849 | +"saint-guillaume-nord": "Lanaudière", | |
| 850 | +"saint-henri": "Chaudière-Appalaches", | |
| 851 | +"saint-henri-de-taillon": "Saguenay–Lac-Saint-Jean", | |
| 852 | +"saint-hermenegilde": "Estrie", | |
| 853 | +"saint-hilaire-de-dorset": "Chaudière-Appalaches", | |
| 854 | +"saint-hilarion": "Capitale-Nationale", | |
| 855 | +"saint-hippolyte": "Laurentides", | |
| 856 | +"saint-honore": "Saguenay–Lac-Saint-Jean", | |
| 857 | +"saint-honore-de-shenley": "Chaudière-Appalaches", | |
| 858 | +"saint-honore-de-temiscouata": "Bas-Saint-Laurent", | |
| 859 | +"saint-hubert": "Montérégie", | |
| 860 | +"saint-hubert-de-riviere-du-loup": "Bas-Saint-Laurent", | |
| 861 | +"saint-hugues": "Montérégie", | |
| 862 | +"saint-hyacinthe": "Montérégie", | |
| 863 | +"saint-ignace-de-loyola": "Lanaudière", | |
| 864 | +"saint-ignace-de-stanbridge": "Estrie", | |
| 865 | +"saint-irenee": "Capitale-Nationale", | |
| 866 | +"saint-isidore": "Montérégie", | |
| 867 | +"saint-isidore-de-clifton": "Estrie", | |
| 868 | +"saint-jacques": "Lanaudière", | |
| 869 | +"saint-jacques-de-leeds": "Chaudière-Appalaches", | |
| 870 | +"saint-jacques-le-majeur-de-wolfestown": "Chaudière-Appalaches", | |
| 871 | +"saint-jacques-le-mineur": "Montérégie", | |
| 872 | +"saint-janvier-de-joly": "Chaudière-Appalaches", | |
| 873 | +"saint-jean-baptiste": "Montérégie", | |
| 874 | +"saint-jean-de-brebeuf": "Chaudière-Appalaches", | |
| 875 | +"saint-jean-de-cherbourg": "Bas-Saint-Laurent", | |
| 876 | +"saint-jean-de-dieu": "Bas-Saint-Laurent", | |
| 877 | +"saint-jean-de-l'ile-d'orleans": "Capitale-Nationale", | |
| 878 | +"saint-jean-de-la-lande": "Bas-Saint-Laurent", | |
| 879 | +"saint-jean-de-matha": "Lanaudière", | |
| 880 | +"saint-jean-port-joli": "Chaudière-Appalaches", | |
| 881 | +"saint-jean-sur-richelieu": "Montérégie", | |
| 882 | +"saint-jerome": "Laurentides", | |
| 883 | +"saint-joachim": "Capitale-Nationale", | |
| 884 | +"saint-joachim-de-shefford": "Estrie", | |
| 885 | +"saint-joseph-de-beauce": "Chaudière-Appalaches", | |
| 886 | +"saint-joseph-de-coleraine": "Chaudière-Appalaches", | |
| 887 | +"saint-joseph-de-kamouraska": "Bas-Saint-Laurent", | |
| 888 | +"saint-joseph-de-lepage": "Bas-Saint-Laurent", | |
| 889 | +"saint-joseph-de-sorel": "Montérégie", | |
| 890 | +"saint-joseph-des-erables": "Chaudière-Appalaches", | |
| 891 | +"saint-joseph-du-lac": "Laurentides", | |
| 892 | +"saint-jude": "Montérégie", | |
| 893 | +"saint-jules": "Chaudière-Appalaches", | |
| 894 | +"saint-julien": "Chaudière-Appalaches", | |
| 895 | +"saint-just-de-bretenieres": "Chaudière-Appalaches", | |
| 896 | +"saint-juste-du-lac": "Bas-Saint-Laurent", | |
| 897 | +"saint-justin": "Mauricie", | |
| 898 | +"saint-lambert": "Montérégie", | |
| 899 | +"saint-lambert-de-lauzon": "Chaudière-Appalaches", | |
| 900 | +"saint-laurent": "Montréal", | |
| 901 | +"saint-laurent-de-l'ile-d'orleans": "Capitale-Nationale", | |
| 902 | +"saint-lazare": "Montérégie", | |
| 903 | +"saint-lazare-de-bellechasse": "Chaudière-Appalaches", | |
| 904 | +"saint-leandre": "Bas-Saint-Laurent", | |
| 905 | +"saint-leon-de-standon": "Chaudière-Appalaches", | |
| 906 | +"saint-leon-le-grand": "Mauricie", | |
| 907 | +"saint-leonard": "Montréal", | |
| 908 | +"saint-leonard-d'aston": "Centre-du-Québec", | |
| 909 | +"saint-leonard-de-portneuf": "Capitale-Nationale", | |
| 910 | +"saint-liboire": "Montérégie", | |
| 911 | +"saint-liguori": "Lanaudière", | |
| 912 | +"saint-lin--laurentides": "Lanaudière", | |
| 913 | +"saint-louis": "Montérégie", | |
| 914 | +"saint-louis-de-blandford": "Centre-du-Québec", | |
| 915 | +"saint-louis-de-gonzague": "Montérégie", | |
| 916 | +"saint-louis-de-gonzague-du-cap-tourmente": "Capitale-Nationale", | |
| 917 | +"saint-louis-du-ha! ha!": "Bas-Saint-Laurent", | |
| 918 | +"saint-luc-de-bellechasse": "Chaudière-Appalaches", | |
| 919 | +"saint-luc-de-vincennes": "Mauricie", | |
| 920 | +"saint-lucien": "Centre-du-Québec", | |
| 921 | +"saint-ludger": "Estrie", | |
| 922 | +"saint-ludger-de-milot": "Saguenay–Lac-Saint-Jean", | |
| 923 | +"saint-magloire": "Chaudière-Appalaches", | |
| 924 | +"saint-majorique-de-grantham": "Centre-du-Québec", | |
| 925 | +"saint-malachie": "Chaudière-Appalaches", | |
| 926 | +"saint-malo": "Estrie", | |
| 927 | +"saint-marc-de-figuery": "Abitibi-Témiscamingue", | |
| 928 | +"saint-marc-des-carrieres": "Capitale-Nationale", | |
| 929 | +"saint-marc-du-lac-long": "Bas-Saint-Laurent", | |
| 930 | +"saint-marc-sur-richelieu": "Montérégie", | |
| 931 | +"saint-marcel": "Chaudière-Appalaches", | |
| 932 | +"saint-marcel-de-richelieu": "Montérégie", | |
| 933 | +"saint-marcellin": "Bas-Saint-Laurent", | |
| 934 | +"saint-martin": "Chaudière-Appalaches", | |
| 935 | +"saint-mathias-sur-richelieu": "Montérégie", | |
| 936 | +"saint-mathieu": "Montérégie", | |
| 937 | +"saint-mathieu-d'harricana": "Abitibi-Témiscamingue", | |
| 938 | +"saint-mathieu-de-beloeil": "Montérégie", | |
| 939 | +"saint-mathieu-de-rioux": "Bas-Saint-Laurent", | |
| 940 | +"saint-mathieu-du-parc": "Mauricie", | |
| 941 | +"saint-maurice": "Mauricie", | |
| 942 | +"saint-maxime-du-mont-louis": "Gaspésie–Îles-de-la-Madeleine", | |
| 943 | +"saint-medard": "Bas-Saint-Laurent", | |
| 944 | +"saint-michel": "Montérégie", | |
| 945 | +"saint-michel-de-bellechasse": "Chaudière-Appalaches", | |
| 946 | +"saint-michel-des-saints": "Lanaudière", | |
| 947 | +"saint-michel-du-squatec": "Bas-Saint-Laurent", | |
| 948 | +"saint-modeste": "Bas-Saint-Laurent", | |
| 949 | +"saint-moise": "Bas-Saint-Laurent", | |
| 950 | +"saint-narcisse": "Mauricie", | |
| 951 | +"saint-narcisse-de-beaurivage": "Chaudière-Appalaches", | |
| 952 | +"saint-narcisse-de-rimouski": "Bas-Saint-Laurent", | |
| 953 | +"saint-nazaire": "Saguenay–Lac-Saint-Jean", | |
| 954 | +"saint-nazaire-d'acton": "Montérégie", | |
| 955 | +"saint-nazaire-de-dorchester": "Chaudière-Appalaches", | |
| 956 | +"saint-neree-de-bellechasse": "Chaudière-Appalaches", | |
| 957 | +"saint-nicolas": "Chaudière-Appalaches", | |
| 958 | +"saint-noel": "Bas-Saint-Laurent", | |
| 959 | +"saint-norbert": "Lanaudière", | |
| 960 | +"saint-norbert-d'arthabaska": "Centre-du-Québec", | |
| 961 | +"saint-octave-de-metis": "Bas-Saint-Laurent", | |
| 962 | +"saint-odilon-de-cranbourne": "Chaudière-Appalaches", | |
| 963 | +"saint-omer": "Chaudière-Appalaches", | |
| 964 | +"saint-ours": "Montérégie", | |
| 965 | +"saint-pacome": "Bas-Saint-Laurent", | |
| 966 | +"saint-pamphile": "Chaudière-Appalaches", | |
| 967 | +"saint-pascal": "Bas-Saint-Laurent", | |
| 968 | +"saint-patrice-de-beaurivage": "Chaudière-Appalaches", | |
| 969 | +"saint-patrice-de-sherrington": "Montérégie", | |
| 970 | +"saint-paul": "Lanaudière", | |
| 971 | +"saint-paul-d'abbotsford": "Montérégie", | |
| 972 | +"saint-paul-de-l'ile-aux-noix": "Montérégie", | |
| 973 | +"saint-paul-de-la-croix": "Bas-Saint-Laurent", | |
| 974 | +"saint-paul-de-montminy": "Chaudière-Appalaches", | |
| 975 | +"saint-paulin": "Mauricie", | |
| 976 | +"saint-philemon": "Chaudière-Appalaches", | |
| 977 | +"saint-philibert": "Chaudière-Appalaches", | |
| 978 | +"saint-philippe": "Montérégie", | |
| 979 | +"saint-philippe-de-neri": "Bas-Saint-Laurent", | |
| 980 | +"saint-pie": "Montérégie", | |
| 981 | +"saint-pie-de-guire": "Centre-du-Québec", | |
| 982 | +"saint-pierre": "Lanaudière", | |
| 983 | +"saint-pierre-baptiste": "Centre-du-Québec", | |
| 984 | +"saint-pierre-de-broughton": "Chaudière-Appalaches", | |
| 985 | +"saint-pierre-de-l'ile-d'orleans": "Capitale-Nationale", | |
| 986 | +"saint-pierre-de-la-riviere-du-sud": "Chaudière-Appalaches", | |
| 987 | +"saint-pierre-de-lamy": "Bas-Saint-Laurent", | |
| 988 | +"saint-pierre-les-becquets": "Centre-du-Québec", | |
| 989 | +"saint-placide": "Laurentides", | |
| 990 | +"saint-polycarpe": "Montérégie", | |
| 991 | +"saint-prime": "Saguenay–Lac-Saint-Jean", | |
| 992 | +"saint-prosper": "Chaudière-Appalaches", | |
| 993 | +"saint-prosper-de-champlain": "Mauricie", | |
| 994 | +"saint-raphael": "Chaudière-Appalaches", | |
| 995 | +"saint-raymond": "Capitale-Nationale", | |
| 996 | +"saint-remi": "Montérégie", | |
| 997 | +"saint-remi-de-tingwick": "Centre-du-Québec", | |
| 998 | +"saint-rene": "Chaudière-Appalaches", | |
| 999 | +"saint-rene-de-matane": "Bas-Saint-Laurent", | |
| 1000 | +"saint-robert": "Montérégie", | |
| 1001 | +"saint-robert-bellarmin": "Estrie", | |
| 1002 | +"saint-roch-de-l'achigan": "Lanaudière", | |
| 1003 | +"saint-roch-de-mekinac": "Mauricie", | |
| 1004 | +"saint-roch-de-richelieu": "Montérégie", | |
| 1005 | +"saint-roch-des-aulnaies": "Chaudière-Appalaches", | |
| 1006 | +"saint-roch-ouest": "Lanaudière", | |
| 1007 | +"saint-romain": "Estrie", | |
| 1008 | +"saint-romuald": "Chaudière-Appalaches", | |
| 1009 | +"saint-rosaire": "Centre-du-Québec", | |
| 1010 | +"saint-samuel": "Centre-du-Québec", | |
| 1011 | +"saint-sauveur": "Laurentides", | |
| 1012 | +"saint-sebastien": "Estrie", | |
| 1013 | +"saint-severe": "Mauricie", | |
| 1014 | +"saint-severin": "Mauricie", | |
| 1015 | +"saint-simeon": "Gaspésie–Îles-de-la-Madeleine", | |
| 1016 | +"saint-simon": "Montérégie", | |
| 1017 | +"saint-simon-de-rimouski": "Bas-Saint-Laurent", | |
| 1018 | +"saint-simon-les-mines": "Chaudière-Appalaches", | |
| 1019 | +"saint-sixte": "Outaouais", | |
| 1020 | +"saint-stanislas": "Mauricie", | |
| 1021 | +"saint-stanislas-de-kostka": "Montérégie", | |
| 1022 | +"saint-sulpice": "Lanaudière", | |
| 1023 | +"saint-sylvere": "Centre-du-Québec", | |
| 1024 | +"saint-sylvestre": "Chaudière-Appalaches", | |
| 1025 | +"saint-telesphore": "Montérégie", | |
| 1026 | +"saint-tharcisius": "Bas-Saint-Laurent", | |
| 1027 | +"saint-theodore-d'acton": "Montérégie", | |
| 1028 | +"saint-theophile": "Chaudière-Appalaches", | |
| 1029 | +"saint-thomas": "Lanaudière", | |
| 1030 | +"saint-thomas-didyme": "Saguenay–Lac-Saint-Jean", | |
| 1031 | +"saint-thuribe": "Capitale-Nationale", | |
| 1032 | +"saint-tite": "Mauricie", | |
| 1033 | +"saint-tite-des-caps": "Capitale-Nationale", | |
| 1034 | +"saint-ubalde": "Capitale-Nationale", | |
| 1035 | +"saint-ulric": "Bas-Saint-Laurent", | |
| 1036 | +"saint-urbain": "Capitale-Nationale", | |
| 1037 | +"saint-urbain-premier": "Montérégie", | |
| 1038 | +"saint-valentin": "Montérégie", | |
| 1039 | +"saint-valere": "Centre-du-Québec", | |
| 1040 | +"saint-valerien": "Bas-Saint-Laurent", | |
| 1041 | +"saint-valerien-de-milton": "Montérégie", | |
| 1042 | +"saint-vallier": "Chaudière-Appalaches", | |
| 1043 | +"saint-venant-de-paquette": "Estrie", | |
| 1044 | +"saint-vianney": "Bas-Saint-Laurent", | |
| 1045 | +"saint-victor": "Chaudière-Appalaches", | |
| 1046 | +"saint-wenceslas": "Centre-du-Québec", | |
| 1047 | +"saint-zacharie": "Chaudière-Appalaches", | |
| 1048 | +"saint-zenon": "Lanaudière", | |
| 1049 | +"saint-zenon-du-lac-humqui": "Bas-Saint-Laurent", | |
| 1050 | +"saint-zephirin-de-courval": "Centre-du-Québec", | |
| 1051 | +"saint-zotique": "Montérégie", | |
| 1052 | +"sainte-adele": "Laurentides", | |
| 1053 | +"sainte-agathe-de-lotbiniere": "Chaudière-Appalaches", | |
| 1054 | +"sainte-agathe-des-monts": "Laurentides", | |
| 1055 | +"sainte-angele-de-merici": "Bas-Saint-Laurent", | |
| 1056 | +"sainte-angele-de-monnoir": "Montérégie", | |
| 1057 | +"sainte-angele-de-premont": "Mauricie", | |
| 1058 | +"sainte-anne-de-beaupre": "Capitale-Nationale", | |
| 1059 | +"sainte-anne-de-bellevue": "Montréal", | |
| 1060 | +"sainte-anne-de-la-perade": "Mauricie", | |
| 1061 | +"sainte-anne-de-la-rochelle": "Estrie", | |
| 1062 | +"sainte-anne-de-sabrevois": "Montérégie", | |
| 1063 | +"sainte-anne-de-sorel": "Montérégie", | |
| 1064 | +"sainte-anne-des-lacs": "Laurentides", | |
| 1065 | +"sainte-anne-des-monts": "Gaspésie–Îles-de-la-Madeleine", | |
| 1066 | +"sainte-anne-des-plaines": "Laurentides", | |
| 1067 | +"sainte-anne-du-lac": "Laurentides", | |
| 1068 | +"sainte-apolline-de-patton": "Chaudière-Appalaches", | |
| 1069 | +"sainte-aurelie": "Chaudière-Appalaches", | |
| 1070 | +"sainte-barbe": "Montérégie", | |
| 1071 | +"sainte-beatrix": "Lanaudière", | |
| 1072 | +"sainte-brigide-d'iberville": "Montérégie", | |
| 1073 | +"sainte-brigitte-de-laval": "Capitale-Nationale", | |
| 1074 | +"sainte-brigitte-des-saults": "Centre-du-Québec", | |
| 1075 | +"sainte-catherine": "Montérégie", | |
| 1076 | +"sainte-catherine-de-hatley": "Estrie", | |
| 1077 | +"sainte-catherine-de-la-jacques-cartier": "Capitale-Nationale", | |
| 1078 | +"sainte-cecile-de-levrard": "Centre-du-Québec", | |
| 1079 | +"sainte-cecile-de-milton": "Estrie", | |
| 1080 | +"sainte-cecile-de-whitton": "Estrie", | |
| 1081 | +"sainte-christine": "Montérégie", | |
| 1082 | +"sainte-christine-d'auvergne": "Capitale-Nationale", | |
| 1083 | +"sainte-claire": "Chaudière-Appalaches", | |
| 1084 | +"sainte-clotilde": "Montérégie", | |
| 1085 | +"sainte-clotilde-de-beauce": "Chaudière-Appalaches", | |
| 1086 | +"sainte-clotilde-de-horton": "Centre-du-Québec", | |
| 1087 | +"sainte-croix": "Chaudière-Appalaches", | |
| 1088 | +"sainte-edwidge-de-clifton": "Estrie", | |
| 1089 | +"sainte-elisabeth": "Lanaudière", | |
| 1090 | +"sainte-elisabeth-de-proulx": "Saguenay–Lac-Saint-Jean", | |
| 1091 | +"sainte-elizabeth-de-warwick": "Centre-du-Québec", | |
| 1092 | +"sainte-emelie-de-l'energie": "Lanaudière", | |
| 1093 | +"sainte-eulalie": "Centre-du-Québec", | |
| 1094 | +"sainte-euphemie-sur-riviere-du-sud": "Chaudière-Appalaches", | |
| 1095 | +"sainte-famille-de-l'ile-d'orleans": "Capitale-Nationale", | |
| 1096 | +"sainte-felicite": "Bas-Saint-Laurent", | |
| 1097 | +"sainte-flavie": "Bas-Saint-Laurent", | |
| 1098 | +"sainte-florence": "Bas-Saint-Laurent", | |
| 1099 | +"sainte-foy": "Capitale-Nationale", | |
| 1100 | +"sainte-francoise": "Bas-Saint-Laurent", | |
| 1101 | +"sainte-genevieve-de-batiscan": "Mauricie", | |
| 1102 | +"sainte-genevieve-de-berthier": "Lanaudière", | |
| 1103 | +"sainte-germaine-boule": "Abitibi-Témiscamingue", | |
| 1104 | +"sainte-gertrude-manneville": "Abitibi-Témiscamingue", | |
| 1105 | +"sainte-hedwidge": "Saguenay–Lac-Saint-Jean", | |
| 1106 | +"sainte-helene-de-bagot": "Montérégie", | |
| 1107 | +"sainte-helene-de-chester": "Centre-du-Québec", | |
| 1108 | +"sainte-helene-de-kamouraska": "Bas-Saint-Laurent", | |
| 1109 | +"sainte-helene-de-mancebourg": "Abitibi-Témiscamingue", | |
| 1110 | +"sainte-henedine": "Chaudière-Appalaches", | |
| 1111 | +"sainte-irene": "Bas-Saint-Laurent", | |
| 1112 | +"sainte-jeanne-d'arc": "Saguenay–Lac-Saint-Jean", | |
| 1113 | +"sainte-jeanne-d'arc-de-la-mitis": "Bas-Saint-Laurent", | |
| 1114 | +"sainte-julie": "Montérégie", | |
| 1115 | +"sainte-julienne": "Lanaudière", | |
| 1116 | +"sainte-justine": "Chaudière-Appalaches", | |
| 1117 | +"sainte-justine-de-newton": "Montérégie", | |
| 1118 | +"sainte-louise": "Chaudière-Appalaches", | |
| 1119 | +"sainte-luce": "Bas-Saint-Laurent", | |
| 1120 | +"sainte-lucie-de-beauregard": "Chaudière-Appalaches", | |
| 1121 | +"sainte-lucie-des-laurentides": "Laurentides", | |
| 1122 | +"sainte-madeleine": "Montérégie", | |
| 1123 | +"sainte-madeleine-de-la-riviere-madeleine": "Gaspésie–Îles-de-la-Madeleine", | |
| 1124 | +"sainte-marcelline-de-kildare": "Lanaudière", | |
| 1125 | +"sainte-marguerite": "Chaudière-Appalaches", | |
| 1126 | +"sainte-marguerite-du-lac-masson": "Laurentides", | |
| 1127 | +"sainte-marguerite-marie": "Bas-Saint-Laurent", | |
| 1128 | +"sainte-marie": "Chaudière-Appalaches", | |
| 1129 | +"sainte-marie-de-blandford": "Centre-du-Québec", | |
| 1130 | +"sainte-marie-madeleine": "Montérégie", | |
| 1131 | +"sainte-marie-salome": "Lanaudière", | |
| 1132 | +"sainte-marthe": "Montérégie", | |
| 1133 | +"sainte-marthe-sur-le-lac": "Laurentides", | |
| 1134 | +"sainte-martine": "Montérégie", | |
| 1135 | +"sainte-melanie": "Lanaudière", | |
| 1136 | +"sainte-monique": "Saguenay–Lac-Saint-Jean", | |
| 1137 | +"sainte-paule": "Bas-Saint-Laurent", | |
| 1138 | +"sainte-perpetue": "Chaudière-Appalaches", | |
| 1139 | +"sainte-petronille": "Capitale-Nationale", | |
| 1140 | +"sainte-praxede": "Chaudière-Appalaches", | |
| 1141 | +"sainte-rita": "Bas-Saint-Laurent", | |
| 1142 | +"sainte-rose-de-watford": "Chaudière-Appalaches", | |
| 1143 | +"sainte-rose-du-nord": "Saguenay–Lac-Saint-Jean", | |
| 1144 | +"sainte-sabine": "Estrie", | |
| 1145 | +"sainte-seraphine": "Centre-du-Québec", | |
| 1146 | +"sainte-sophie": "Laurentides", | |
| 1147 | +"sainte-sophie-d'halifax": "Centre-du-Québec", | |
| 1148 | +"sainte-sophie-de-levrard": "Centre-du-Québec", | |
| 1149 | +"sainte-thecle": "Mauricie", | |
| 1150 | +"sainte-therese": "Laurentides", | |
| 1151 | +"sainte-therese-de-gaspe": "Gaspésie–Îles-de-la-Madeleine", | |
| 1152 | +"sainte-therese-de-la-gatineau": "Outaouais", | |
| 1153 | +"sainte-ursule": "Mauricie", | |
| 1154 | +"sainte-victoire-de-sorel": "Montérégie", | |
| 1155 | +"saints-anges": "Chaudière-Appalaches", | |
| 1156 | +"saints-martyrs-canadiens": "Centre-du-Québec", | |
| 1157 | +"salaberry-de-valleyfield": "Montérégie", | |
| 1158 | +"salluit": "Nord-du-Québec", | |
| 1159 | +"sault-au-cochon": "Capitale-Nationale", | |
| 1160 | +"sayabec": "Bas-Saint-Laurent", | |
| 1161 | +"schefferville": "Côte-Nord", | |
| 1162 | +"scotstown": "Estrie", | |
| 1163 | +"scott": "Chaudière-Appalaches", | |
| 1164 | +"senneterre": "Abitibi-Témiscamingue", | |
| 1165 | +"senneville": "Montréal", | |
| 1166 | +"sept-iles": "Côte-Nord", | |
| 1167 | +"shannon": "Capitale-Nationale", | |
| 1168 | +"shawinigan": "Mauricie", | |
| 1169 | +"shawville": "Outaouais", | |
| 1170 | +"sheenboro": "Outaouais", | |
| 1171 | +"shefford": "Estrie", | |
| 1172 | +"sherbrooke": "Estrie", | |
| 1173 | +"shigawake": "Gaspésie–Îles-de-la-Madeleine", | |
| 1174 | +"sillery": "Capitale-Nationale", | |
| 1175 | +"sorel-tracy": "Montérégie", | |
| 1176 | +"stanbridge east": "Estrie", | |
| 1177 | +"stanbridge station": "Estrie", | |
| 1178 | +"stanstead": "Estrie", | |
| 1179 | +"stanstead-est": "Estrie", | |
| 1180 | +"stoke": "Estrie", | |
| 1181 | +"stoneham-et-tewkesbury": "Capitale-Nationale", | |
| 1182 | +"stornoway": "Estrie", | |
| 1183 | +"stratford": "Estrie", | |
| 1184 | +"stukely-sud": "Estrie", | |
| 1185 | +"sutton": "Estrie", | |
| 1186 | +"tadoussac": "Côte-Nord", | |
| 1187 | +"taschereau": "Abitibi-Témiscamingue", | |
| 1188 | +"tasiujaq": "Nord-du-Québec", | |
| 1189 | +"temiscaming": "Abitibi-Témiscamingue", | |
| 1190 | +"temiscouata-sur-le-lac": "Bas-Saint-Laurent", | |
| 1191 | +"terrasse-vaudreuil": "Montérégie", | |
| 1192 | +"terrebonne": "Lanaudière", | |
| 1193 | +"thetford mines": "Chaudière-Appalaches", | |
| 1194 | +"thorne": "Outaouais", | |
| 1195 | +"thurso": "Outaouais", | |
| 1196 | +"timiskaming": "Abitibi-Témiscamingue", | |
| 1197 | +"tingwick": "Centre-du-Québec", | |
| 1198 | +"toponyme a venir": "Nord-du-Québec", | |
| 1199 | +"tourville": "Chaudière-Appalaches", | |
| 1200 | +"trecesson": "Abitibi-Témiscamingue", | |
| 1201 | +"tres-saint-redempteur": "Montérégie", | |
| 1202 | +"tres-saint-sacrement": "Montérégie", | |
| 1203 | +"tring-jonction": "Chaudière-Appalaches", | |
| 1204 | +"trois-pistoles": "Bas-Saint-Laurent", | |
| 1205 | +"trois-rives": "Mauricie", | |
| 1206 | +"trois-rivieres": "Mauricie", | |
| 1207 | +"uashat": "Côte-Nord", | |
| 1208 | +"ulverton": "Estrie", | |
| 1209 | +"umiujaq": "Nord-du-Québec", | |
| 1210 | +"upton": "Montérégie", | |
| 1211 | +"val-alain": "Chaudière-Appalaches", | |
| 1212 | +"val-brillant": "Bas-Saint-Laurent", | |
| 1213 | +"val-d'or": "Abitibi-Témiscamingue", | |
| 1214 | +"val-david": "Laurentides", | |
| 1215 | +"val-des-bois": "Outaouais", | |
| 1216 | +"val-des-lacs": "Laurentides", | |
| 1217 | +"val-des-monts": "Outaouais", | |
| 1218 | +"val-des-sources": "Estrie", | |
| 1219 | +"val-joli": "Estrie", | |
| 1220 | +"val-morin": "Laurentides", | |
| 1221 | +"val-racine": "Estrie", | |
| 1222 | +"val-saint-gilles": "Abitibi-Témiscamingue", | |
| 1223 | +"valcourt": "Estrie", | |
| 1224 | +"vallee-jonction": "Chaudière-Appalaches", | |
| 1225 | +"vanier": "Capitale-Nationale", | |
| 1226 | +"varennes": "Montérégie", | |
| 1227 | +"vaudreuil-dorion": "Montérégie", | |
| 1228 | +"vaudreuil-sur-le-lac": "Montérégie", | |
| 1229 | +"venise-en-quebec": "Montérégie", | |
| 1230 | +"vercheres": "Montérégie", | |
| 1231 | +"verdun": "Montréal", | |
| 1232 | +"victoriaville": "Centre-du-Québec", | |
| 1233 | +"vieux-longueuil": "Montérégie", | |
| 1234 | +"ville de quebec": "Capitale-Nationale", | |
| 1235 | +"ville saint-laurent": "Montréal", | |
| 1236 | +"ville-marie": "Abitibi-Témiscamingue", | |
| 1237 | +"villeray": "Montréal", | |
| 1238 | +"villeroy": "Centre-du-Québec", | |
| 1239 | +"waltham": "Outaouais", | |
| 1240 | +"warden": "Estrie", | |
| 1241 | +"warwick": "Centre-du-Québec", | |
| 1242 | +"waskaganish": "Nord-du-Québec", | |
| 1243 | +"waswanipi": "Nord-du-Québec", | |
| 1244 | +"waterloo": "Estrie", | |
| 1245 | +"waterville": "Estrie", | |
| 1246 | +"weedon": "Estrie", | |
| 1247 | +"wemindji": "Nord-du-Québec", | |
| 1248 | +"wemotaci": "Mauricie", | |
| 1249 | +"wendake": "Capitale-Nationale", | |
| 1250 | +"wentworth": "Laurentides", | |
| 1251 | +"wentworth-nord": "Laurentides", | |
| 1252 | +"westbury": "Estrie", | |
| 1253 | +"westmount": "Montréal", | |
| 1254 | +"whapmagoostui": "Nord-du-Québec", | |
| 1255 | +"wickham": "Centre-du-Québec", | |
| 1256 | +"windsor": "Estrie", | |
| 1257 | +"wolinak": "Centre-du-Québec", | |
| 1258 | +"wotton": "Estrie", | |
| 1259 | +"yamachiche": "Mauricie", | |
| 1260 | +"yamaska": "Montérégie" | |
| 1261 | +} | |
| \ No newline at end of file | ||
modified
jobka/db.py
+62 −4
@@ -134,15 +134,23 @@ CREATE TABLE IF NOT EXISTS geocode_cache ( | ||
| 134 | 134 | |
| 135 | 135 | # Colonnes ajoutées après la v1 — migration automatique des bases existantes. |
| 136 | 136 | _MIGRATIONS: dict[str, dict[str, str]] = { |
| 137 | − "jobs": {}, | |
| 137 | + "jobs": { | |
| 138 | + # Phase 2 (2026-08-19) — enrichissement des connecteurs | |
| 139 | + "company_logo": "TEXT", # URL du logo employeur (breezy, JSON-LD…) | |
| 140 | + "language": "TEXT", # fr | en | bilingue (source ou heuristique) | |
| 141 | + "apply_url": "TEXT", # lien de candidature directe | |
| 142 | + "title_clean": "TEXT", # titre d'affichage normalisé (source intacte) | |
| 143 | + "quarantine": "TEXT", # JSON des motifs qualité (NULL = publiable) | |
| 144 | + }, | |
| 138 | 145 | "sync_log": {}, |
| 139 | 146 | } |
| 140 | 147 | |
| 141 | 148 | |
| 142 | 149 | def connect() -> sqlite3.Connection: |
| 143 | 150 | DB_PATH.parent.mkdir(parents=True, exist_ok=True) |
| 144 | − con = sqlite3.connect(DB_PATH) | |
| 151 | + con = sqlite3.connect(DB_PATH, timeout=30.0) | |
| 145 | 152 | con.row_factory = sqlite3.Row |
| 153 | + con.execute("PRAGMA busy_timeout=30000") # BD vivante (sync horaire) | |
| 146 | 154 | con.executescript(_SCHEMA) |
| 147 | 155 | for table, cols in _MIGRATIONS.items(): |
| 148 | 156 | existing = {r["name"] for r in con.execute(f"PRAGMA table_info({table})")} |
@@ -207,12 +215,16 @@ def sync_source(con: sqlite3.Connection, source: str, | ||
| 207 | 215 | |
| 208 | 216 | alert = _drift_alert(con, source, n, null_salary_rate) |
| 209 | 217 | |
| 218 | + from .normalize import clean_title | |
| 219 | + from .quality import issues_for_posting | |
| 220 | + | |
| 210 | 221 | for job in postings: |
| 211 | 222 | seen_uids.add(job.uid) |
| 212 | 223 | h = job.content_hash() |
| 213 | 224 | row = con.execute( |
| 214 | 225 | "SELECT content_hash, salary_min, salary_max FROM jobs WHERE uid=?", |
| 215 | 226 | (job.uid,)).fetchone() |
| 227 | + defects = issues_for_posting(job) | |
| 216 | 228 | params = dict( |
| 217 | 229 | uid=job.uid, source=job.source, external_id=job.external_id, |
| 218 | 230 | url=job.url, employer=job.employer, title=job.title, |
@@ -232,6 +244,11 @@ def sync_source(con: sqlite3.Connection, source: str, | ||
| 232 | 244 | category=job.category, ats=job.ats, |
| 233 | 245 | details=json.dumps(job.details, ensure_ascii=False), |
| 234 | 246 | lat=job.lat, lng=job.lng, content_hash=h, now=now, |
| 247 | + company_logo=job.company_logo, language=job.language, | |
| 248 | + apply_url=job.apply_url, | |
| 249 | + title_clean=clean_title(job.title, job.city), | |
| 250 | + quarantine=json.dumps(defects, ensure_ascii=False) | |
| 251 | + if defects else None, | |
| 235 | 252 | ) |
| 236 | 253 | if row is None: |
| 237 | 254 | con.execute( |
@@ -242,7 +259,8 @@ def sync_source(con: sqlite3.Connection, source: str, | ||
| 242 | 259 | salary_year_max, salary_hour_min, salary_hour_max, benefits, |
| 243 | 260 | requirements, date_posted, date_deadline, category, ats, |
| 244 | 261 | details, lat, lng, content_hash, first_seen, last_seen, |
| 245 | − updated_at, miss_count, active) | |
| 262 | + updated_at, miss_count, active, company_logo, language, | |
| 263 | + apply_url, title_clean, quarantine) | |
| 246 | 264 | VALUES (:uid,:source,:external_id,:url,:employer,:title, |
| 247 | 265 | :description,:address,:city,:region,:postal_code, |
| 248 | 266 | :location_label,:work_mode,:employment_type,:salary_min, |
@@ -250,7 +268,8 @@ def sync_source(con: sqlite3.Connection, source: str, | ||
| 250 | 268 | :salary_year_max,:salary_hour_min,:salary_hour_max, |
| 251 | 269 | :benefits,:requirements,:date_posted,:date_deadline, |
| 252 | 270 | :category,:ats,:details,:lat,:lng,:content_hash, |
| 253 | − :now,:now,:now,0,1)""", params) | |
| 271 | + :now,:now,:now,0,1,:company_logo,:language,:apply_url, | |
| 272 | + :title_clean,:quarantine)""", params) | |
| 254 | 273 | if job.salary_min is not None: # point de départ de l'historique |
| 255 | 274 | con.execute( |
| 256 | 275 | "INSERT INTO salary_log (uid, ts, s_min, s_max, unit)" |
@@ -279,6 +298,9 @@ def sync_source(con: sqlite3.Connection, source: str, | ||
| 279 | 298 | date_posted=:date_posted, date_deadline=:date_deadline, |
| 280 | 299 | category=:category, ats=:ats, details=:details, |
| 281 | 300 | lat=COALESCE(:lat, lat), lng=COALESCE(:lng, lng), |
| 301 | + company_logo=:company_logo, language=:language, | |
| 302 | + apply_url=:apply_url, title_clean=:title_clean, | |
| 303 | + quarantine=:quarantine, | |
| 282 | 304 | content_hash=:content_hash, last_seen=:now, |
| 283 | 305 | updated_at=:now, miss_count=0, active=1 |
| 284 | 306 | WHERE uid=:uid""", params) |
@@ -344,6 +366,26 @@ def expire_past_deadline(con: sqlite3.Connection) -> int: | ||
| 344 | 366 | return cur.rowcount |
| 345 | 367 | |
| 346 | 368 | |
| 369 | +STALE_DAYS = 7 # offres plus jamais revues (connecteur retiré/muet) : archivées | |
| 370 | + | |
| 371 | + | |
| 372 | +def expire_stale(con: sqlite3.Connection, days: int = STALE_DAYS) -> int: | |
| 373 | + """Archive (active=0) les offres actives non revues depuis `days` jours. | |
| 374 | + | |
| 375 | + Filet de sécurité complémentaire au délai de grâce MISS_GRACE : couvre le | |
| 376 | + cas d'un connecteur SUPPRIMÉ du code (ses offres ne passent plus jamais | |
| 377 | + par sync_source et resteraient actives pour toujours — cas « valero »). | |
| 378 | + Rien n'est supprimé : les offres sont archivées, réactivables si revues. | |
| 379 | + """ | |
| 380 | + cutoff = time.time() - days * 86400 | |
| 381 | + cur = con.execute( | |
| 382 | + "UPDATE jobs SET active=0, updated_at=? WHERE active=1 AND last_seen<?" | |
| 383 | + " AND source<>'depot-direct'", # le dépôt direct n'est jamais resynchronisé | |
| 384 | + (time.time(), cutoff)) | |
| 385 | + con.commit() | |
| 386 | + return cur.rowcount | |
| 387 | + | |
| 388 | + | |
| 347 | 389 | def log_failure(con: sqlite3.Connection, source: str, message: str) -> None: |
| 348 | 390 | con.execute( |
| 349 | 391 | "INSERT INTO sync_log (source, ts, found, added, updated, removed, ok, message)" |
@@ -369,6 +411,22 @@ def get_cached_detail(con: sqlite3.Connection, source: str, | ||
| 369 | 411 | return None |
| 370 | 412 | |
| 371 | 413 | |
| 414 | +def get_stale_detail(con: sqlite3.Connection, source: str, | |
| 415 | + external_id: str) -> dict | None: | |
| 416 | + """Payload détail en cache MÊME si la clé a changé — secours quand le | |
| 417 | + budget de re-visites est épuisé : un détail périmé vaut mieux qu'une | |
| 418 | + description vide (la re-visite aura lieu à un prochain cycle).""" | |
| 419 | + row = con.execute( | |
| 420 | + "SELECT payload FROM detail_cache WHERE source=? AND external_id=?", | |
| 421 | + (source, external_id)).fetchone() | |
| 422 | + if row and row["payload"]: | |
| 423 | + try: | |
| 424 | + return json.loads(row["payload"]) | |
| 425 | + except ValueError: | |
| 426 | + return None | |
| 427 | + return None | |
| 428 | + | |
| 429 | + | |
| 372 | 430 | def put_cached_detail(con: sqlite3.Connection, source: str, |
| 373 | 431 | external_id: str, key: str, payload: dict) -> None: |
| 374 | 432 | con.execute( |
modified
jobka/ingest.py
+9 −0
@@ -56,6 +56,15 @@ def run(sources: list[str] | None = None) -> list[dict]: | ||
| 56 | 56 | print(f"[job-ka] date limite dépassée : {expired} offre(s) retirée(s)") |
| 57 | 57 | except Exception: |
| 58 | 58 | pass |
| 59 | + # offres orphelines (connecteur retiré/muet) : archivées après STALE_DAYS | |
| 60 | + # — seulement lors d'un passage complet (un sync ciblé ne juge pas les autres) | |
| 61 | + if not sources: | |
| 62 | + try: | |
| 63 | + stale = db.expire_stale(con) | |
| 64 | + if stale: | |
| 65 | + print(f"[job-ka] offres orphelines archivées : {stale}") | |
| 66 | + except Exception: | |
| 67 | + pass | |
| 59 | 68 | # statistiques du planificateur SQLite (requêtes bbox de la carte) |
| 60 | 69 | try: |
| 61 | 70 | con.execute("PRAGMA optimize") |
modified
jobka/normalize.py
+234 −6
@@ -21,9 +21,11 @@ import unicodedata | ||
| 21 | 21 | |
| 22 | 22 | __all__ = [ |
| 23 | 23 | "strip_accents", "clean_text", "clean_html", "parse_salary", |
| 24 | − "salary_to_yearly", "salary_to_hourly", "parse_date", "parse_work_mode", | |
| 24 | + "sanitize_salary", "salary_from_text", "salary_to_yearly", | |
| 25 | + "salary_to_hourly", "parse_date", "parse_work_mode", | |
| 25 | 26 | "parse_employment_type", "parse_location", "canonical_city", |
| 26 | − "is_quebec_location", "categorize", | |
| 27 | + "is_quebec_location", "categorize", "detect_language", | |
| 28 | + "extract_benefits", "clean_title", | |
| 27 | 29 | ] |
| 28 | 30 | |
| 29 | 31 | |
@@ -50,6 +52,12 @@ def clean_text(s: str) -> str: | ||
| 50 | 52 | _BLOCK_TAGS = re.compile( |
| 51 | 53 | r"</?(?:p|div|br|li|ul|ol|h[1-6]|tr|table|section|article)[^>]*>", re.I) |
| 52 | 54 | |
| 55 | +# Résidu HTML après une passe : balise ouvrante plausible ou entité encore | |
| 56 | +# encodée — signe d'un contenu doublement échappé. | |
| 57 | +_HTML_LEFTOVER = re.compile( | |
| 58 | + r"<(?:p|br|li|ul|ol|div|span|strong|em|b|i|h[1-6]|a)\b|&(?:amp|lt|gt|nbsp|#\d+);", | |
| 59 | + re.I) | |
| 60 | + | |
| 53 | 61 | |
| 54 | 62 | def clean_html(html: str) -> str: |
| 55 | 63 | """HTML -> texte lisible : balises de bloc = sauts de ligne, le reste retiré. |
@@ -60,10 +68,16 @@ def clean_html(html: str) -> str: | ||
| 60 | 68 | if not html: |
| 61 | 69 | return "" |
| 62 | 70 | import html as _html |
| 63 | − s = _html.unescape(html) | |
| 64 | − s = re.sub(r"<(script|style)[^>]*>.*?</\1>", " ", s, flags=re.I | re.S) | |
| 65 | − s = _BLOCK_TAGS.sub("\n", s) | |
| 66 | − s = re.sub(r"<[^>]+>", " ", s) | |
| 71 | + s = html | |
| 72 | + # Contenus sur-échappés (entités dans les entités — Greenhouse/Lever) : | |
| 73 | + # nettoyer en boucle tant qu'il reste des balises/entités (max 3 passes). | |
| 74 | + for _ in range(3): | |
| 75 | + s = _html.unescape(s) | |
| 76 | + s = re.sub(r"<(script|style)[^>]*>.*?</\1>", " ", s, flags=re.I | re.S) | |
| 77 | + s = _BLOCK_TAGS.sub("\n", s) | |
| 78 | + s = re.sub(r"<[^>]+>", " ", s) | |
| 79 | + if not _HTML_LEFTOVER.search(s): | |
| 80 | + break | |
| 67 | 81 | s = s.replace("\xa0", " ").replace(" ", " ") |
| 68 | 82 | s = re.sub(r"[ \t]+", " ", s) |
| 69 | 83 | s = re.sub(r" ?\n ?", "\n", s) |
@@ -176,6 +190,67 @@ def salary_to_hourly(value: float | None, unit: str | None) -> float | None: | ||
| 176 | 190 | return round(y / _HOURS_PER_YEAR, 2) if y is not None else None |
| 177 | 191 | |
| 178 | 192 | |
| 193 | +def sanitize_salary(lo: float | None, hi: float | None, unit: str | None | |
| 194 | + ) -> tuple[float | None, float | None, str | None]: | |
| 195 | + """Bornes de plausibilité sur les salaires STRUCTURÉS fournis par les ATS. | |
| 196 | + | |
| 197 | + Certaines sources étiquettent mal l'unité (75 000 « /mois » ou « /h » pour | |
| 198 | + un salaire annuel — vu chez Lever et dans les JSON-LD de portails) : si la | |
| 199 | + valeur sort des bornes de son unité, on tente une réinterprétation vers | |
| 200 | + l'unité dont les bornes la contiennent (année d'abord, puis heure). Aucune | |
| 201 | + unité plausible -> on jette (pas de salaire aberrant publié). | |
| 202 | + """ | |
| 203 | + if lo is None or unit not in _SALARY_BOUNDS: | |
| 204 | + return (lo, hi, unit) | |
| 205 | + b = _SALARY_BOUNDS[unit] | |
| 206 | + if not (b[0] <= lo <= b[1]): | |
| 207 | + for candidate in ("year", "hour"): | |
| 208 | + cb = _SALARY_BOUNDS[candidate] | |
| 209 | + if candidate != unit and cb[0] <= lo <= cb[1]: | |
| 210 | + unit, b = candidate, cb | |
| 211 | + break | |
| 212 | + else: | |
| 213 | + return (None, None, None) | |
| 214 | + if hi is not None and not (b[0] <= hi <= b[1]): | |
| 215 | + hi = None # max aberrant : on garde le min plausible seul | |
| 216 | + if hi is not None and hi < lo: | |
| 217 | + lo, hi = hi, lo | |
| 218 | + return (lo, hi, unit) | |
| 219 | + | |
| 220 | + | |
| 221 | +# Contextes où un montant en $ n'est PAS un salaire (financement, dépenses, | |
| 222 | +# allocations…) — garde-fou de l'extraction depuis la description. | |
| 223 | +_NOT_SALARY_CONTEXT = re.compile( | |
| 224 | + r"financement|funding|amorcage|seed|investisse|invest(?:ment|or)|" | |
| 225 | + r"revenu[s]?\b|revenue|chiffre d affaires|budget|remboursement|" | |
| 226 | + r"reimburse|expense|allocation|allowance|subvention|credit d impot|" | |
| 227 | + r"cotisation|rabais|discount|gift|carte[- ]cadeau|referral (?:bonus|fee)|" | |
| 228 | + r"signing bonus|prime (?:a l embauche|de demarrage|de reference)|" | |
| 229 | + r"million|milliard") | |
| 230 | + | |
| 231 | + | |
| 232 | +def salary_from_text(text: str) -> tuple[float | None, float | None, | |
| 233 | + str | None, str]: | |
| 234 | + """Cherche une mention salariale dans un texte libre (description). | |
| 235 | + | |
| 236 | + Découpe en phrases (les points décimaux « 182,500.00 » ne coupent pas), | |
| 237 | + ignore les contextes non salariaux (financement, remboursements…), puis | |
| 238 | + parse la première phrase plausible. Retourne (min, max, unité, libellé). | |
| 239 | + """ | |
| 240 | + if not text or "$" not in text: | |
| 241 | + return (None, None, None, "") | |
| 242 | + for sentence in re.split(r"(?<!\d)\.(?!\d)|\n|;", text): | |
| 243 | + if "$" not in sentence or not re.search(r"\d", sentence): | |
| 244 | + continue | |
| 245 | + frag = clean_text(sentence)[:160] | |
| 246 | + if _NOT_SALARY_CONTEXT.search(_key(frag)): | |
| 247 | + continue | |
| 248 | + lo, hi, unit = parse_salary(frag) | |
| 249 | + if lo is not None: | |
| 250 | + return (lo, hi, unit, frag) | |
| 251 | + return (None, None, None, "") | |
| 252 | + | |
| 253 | + | |
| 179 | 254 | # --------------------------------------------------------------------------- |
| 180 | 255 | # Dates — ISO 8601, jamais de date inventée |
| 181 | 256 | # --------------------------------------------------------------------------- |
@@ -499,6 +574,159 @@ _CATEGORY_RULES = [ | ||
| 499 | 574 | ] |
| 500 | 575 | |
| 501 | 576 | |
| 577 | +# --------------------------------------------------------------------------- | |
| 578 | +# Langue de l'offre — heuristique légère par mots-outils (fr | en | bilingue) | |
| 579 | +# --------------------------------------------------------------------------- | |
| 580 | + | |
| 581 | +# Mots-outils très fréquents et quasi exclusifs à chaque langue (sans accents). | |
| 582 | +_FR_STOPWORDS = re.compile( | |
| 583 | + r"\b(?:le|la|les|des|une|et|est|sont|pour|avec|vous|nous|notre|nos|vos|" | |
| 584 | + r"dans|chez|aux|du|au|ainsi|plus de|afin|etre|sera|poste|equipe|" | |
| 585 | + r"exigences|taches|milieu|travail)\b") | |
| 586 | +_EN_STOPWORDS = re.compile( | |
| 587 | + r"\b(?:the|and|for|with|you|your|we|our|is|are|will|this|that|team|" | |
| 588 | + r"work|skills|of|to be|as well|role|responsibilities|requirements|" | |
| 589 | + r"experience in|ability)\b") | |
| 590 | + | |
| 591 | + | |
| 592 | +def detect_language(title: str, description: str = "") -> str: | |
| 593 | + """Langue du texte de l'offre : « fr », « en », « bilingue », ou "". | |
| 594 | + | |
| 595 | + Comptage de mots-outils sur le début du texte — déterministe, jamais de | |
| 596 | + valeur inventée : texte trop court ou ambigu -> "". | |
| 597 | + """ | |
| 598 | + text = _key(f"{title}\n{description or ''}"[:4000]) | |
| 599 | + if len(text) < 30: | |
| 600 | + return "" | |
| 601 | + fr = len(_FR_STOPWORDS.findall(text)) | |
| 602 | + en = len(_EN_STOPWORDS.findall(text)) | |
| 603 | + total = fr + en | |
| 604 | + if total < 5: | |
| 605 | + return "" | |
| 606 | + # les deux langues nettement présentes -> offre bilingue | |
| 607 | + if fr >= 8 and en >= 8 and min(fr, en) / max(fr, en) >= 0.35: | |
| 608 | + return "bilingue" | |
| 609 | + if fr > en: | |
| 610 | + return "fr" | |
| 611 | + if en > fr: | |
| 612 | + return "en" | |
| 613 | + return "" | |
| 614 | + | |
| 615 | + | |
| 616 | +# --------------------------------------------------------------------------- | |
| 617 | +# Avantages — extraction des blocs « Avantages / Benefits » des descriptions | |
| 618 | +# --------------------------------------------------------------------------- | |
| 619 | + | |
| 620 | +_BENEFITS_HEADING = re.compile( | |
| 621 | + r"^\s*(?:les\s+|nos\s+|our\s+|tes\s+|vos\s+)?(?:avantages(?:\s+sociaux)?|" | |
| 622 | + r"benefits(?:\s+&\s+perks)?|perks(?:\s+(?:and|&)\s+benefits)?|" | |
| 623 | + r"ce que nous (?:t')?offrons|what we offer|why join us|" | |
| 624 | + r"pourquoi nous rejoindre|nous offrons|we offer)\s*:?\s*$", re.I) | |
| 625 | +_BENEFIT_LINE_MAX = 120 | |
| 626 | + | |
| 627 | + | |
| 628 | +def extract_benefits(description: str) -> list[str]: | |
| 629 | + """Liste d'avantages depuis un bloc structuré de la description. | |
| 630 | + | |
| 631 | + Cherche un titre de section « Avantages/Benefits/… » seul sur sa ligne, | |
| 632 | + puis prend les lignes courtes qui suivent (puces), en s'arrêtant à la | |
| 633 | + première ligne longue ou au prochain titre. Max 12 items — si le bloc ne | |
| 634 | + ressemble pas à une liste, on ne retourne rien (pas d'à-peu-près). | |
| 635 | + """ | |
| 636 | + if not description: | |
| 637 | + return [] | |
| 638 | + lines = description.split("\n") | |
| 639 | + for i, line in enumerate(lines): | |
| 640 | + if not _BENEFITS_HEADING.match(strip_accents(line)): | |
| 641 | + continue | |
| 642 | + items: list[str] = [] | |
| 643 | + for nxt in lines[i + 1:]: | |
| 644 | + t = clean_text(nxt).lstrip("•-–—*·◦▪ ").strip() | |
| 645 | + if not t: | |
| 646 | + if items: | |
| 647 | + break # fin du bloc (ligne vide après les puces) | |
| 648 | + continue | |
| 649 | + if len(t) > _BENEFIT_LINE_MAX or _BENEFITS_HEADING.match( | |
| 650 | + strip_accents(t)): | |
| 651 | + break | |
| 652 | + # un « titre » de section suivant (courte ligne sans ponctuation | |
| 653 | + # finale, capitalisée) termine aussi le bloc | |
| 654 | + if items and len(t.split()) <= 4 and t.endswith(":"): | |
| 655 | + break | |
| 656 | + items.append(t.rstrip(" ;,.").strip()) | |
| 657 | + if len(items) >= 12: | |
| 658 | + break | |
| 659 | + if len(items) >= 2: | |
| 660 | + return items | |
| 661 | + return [] | |
| 662 | + | |
| 663 | + | |
| 664 | +# --------------------------------------------------------------------------- | |
| 665 | +# Titre d'affichage — casse propre, codes de réquisition et suffixes retirés | |
| 666 | +# --------------------------------------------------------------------------- | |
| 667 | + | |
| 668 | +# codes de réquisition : « [R-12345] », « (REQ 2026-42) », « - JR0035678 »… | |
| 669 | +_REQ_CODE = re.compile( | |
| 670 | + # en tête : « 123456 - Titre », « [REQ-1234] Titre » (≥5 chiffres nus pour | |
| 671 | + # ne pas manger une année « 2026 - Programme d'été ») | |
| 672 | + r"^\s*[\[(]?(?:req(?:uisition)?|jr|job)[\s#:-]*\d{3,10}[\])]?\s*[-–—:|]\s*|" | |
| 673 | + r"^\s*[\[(]?\d{5,10}[\])]?\s*[-–—:|]\s*|" | |
| 674 | + # en queue : « Titre - R-12345 », « Titre (JOB 004521) » | |
| 675 | + r"\s*[-–—|(]\s*(?:req(?:uisition)?|job|poste)?\s*#?\s*(?:[A-Z]{1,4}-?)?\d{4,10}\s*[)\]]?\s*$|" | |
| 676 | + # au milieu : « Titre [R-12345] suite » | |
| 677 | + r"\s*[\[(](?:R|REQ|JR|JOB)[-_ ]?\d{3,10}[\])]\s*", re.I) | |
| 678 | + | |
| 679 | +# petits mots gardés en minuscules lors de la remise en casse d'un titre | |
| 680 | +# tout-majuscules ; sigles usuels gardés en majuscules | |
| 681 | +_TITLE_SMALL = {"de", "du", "des", "la", "le", "les", "et", "en", "à", "a", | |
| 682 | + "au", "aux", "d'", "l'", "of", "the", "and", "in", "for", | |
| 683 | + "to", "or", "ou", "un", "une", "sur", "pour"} | |
| 684 | +_TITLE_ACRONYMS = {"rh", "ti", "it", "qa", "qc", "pab", "cpa", "cfa", "erp", | |
| 685 | + "sap", "crm", "sst", "cnc", "hvac", "rpa", "bi", "ia", | |
| 686 | + "ai", "vp", "pdg", "dg"} | |
| 687 | + | |
| 688 | + | |
| 689 | +def _smart_case(title: str) -> str: | |
| 690 | + """Titre TOUT EN MAJUSCULES -> casse de phrase lisible (sigles préservés).""" | |
| 691 | + words = title.lower().split() | |
| 692 | + out = [] | |
| 693 | + for i, w in enumerate(words): | |
| 694 | + base = w.strip("()[],.;:/") | |
| 695 | + if base in _TITLE_ACRONYMS: | |
| 696 | + out.append(w.replace(base, base.upper())) | |
| 697 | + elif i > 0 and base in _TITLE_SMALL: | |
| 698 | + out.append(w) | |
| 699 | + else: | |
| 700 | + out.append(w[:1].upper() + w[1:]) | |
| 701 | + return " ".join(out) | |
| 702 | + | |
| 703 | + | |
| 704 | +def clean_title(title: str, city: str = "") -> str: | |
| 705 | + """Titre d'affichage : codes de réquisition retirés, suffixe « - <ville> » | |
| 706 | + redondant retiré, casse corrigée si tout-majuscules. Le titre SOURCE n'est | |
| 707 | + jamais modifié — ceci alimente une colonne d'affichage distincte.""" | |
| 708 | + t = clean_text(title) | |
| 709 | + if not t: | |
| 710 | + return "" | |
| 711 | + prev = None | |
| 712 | + while prev != t: # les codes peuvent être empilés | |
| 713 | + prev = t | |
| 714 | + t = _REQ_CODE.sub(" ", t).strip(" -–—|:") | |
| 715 | + t = clean_text(t) | |
| 716 | + # suffixe « - Montréal » / « (Montréal, QC) » identique à la ville de | |
| 717 | + # l'offre : repéré sans accents, coupé sur le titre original | |
| 718 | + if city: | |
| 719 | + ck = re.escape(strip_accents(city.lower())) | |
| 720 | + m = re.search( | |
| 721 | + rf"\s*[-–—|,(]\s*(?:ville de\s+)?{ck}(?:\s*,\s*(?:qc|quebec))?\s*\)?\s*$", | |
| 722 | + strip_accents(t.lower())) | |
| 723 | + if m and m.start() > 0: | |
| 724 | + t = clean_text(t[:m.start()]).strip(" -–—|,(") | |
| 725 | + if len(t) > 8 and t == t.upper() and t != t.lower(): | |
| 726 | + t = _smart_case(t) | |
| 727 | + return t or clean_text(title) | |
| 728 | + | |
| 729 | + | |
| 502 | 730 | def categorize(title: str, description: str = "") -> str: |
| 503 | 731 | """Titre -> catégorie interne (repli : début de description), ou "". |
| 504 | 732 | |
added
jobka/quality.py
+82 −0
Large diff (82 lines) is not loaded by default to keep the page fast.Load diff
added
jobka/regions.py
+87 −0
Large diff (87 lines) is not loaded by default to keep the page fast.Load diff
modified
jobka/schema.py
+35 −8
Large diff (81 lines) is not loaded by default to keep the page fast.Load diff
modified
tests/test_normalize.py
+149 −0
Large diff (153 lines) is not loaded by default to keep the page fast.Load diff