SPB Git

spb/trouve-ka Public

Trouve-KA — moteur de recherche web indépendant, Québec-first. Crawler distribué, index OpenSearch, ranking bilingue, galerie d'images. En prod : www.trouve-ka.com

Python 76.8% TypeScript 15.7% SQL 3.9% Shell 1.4% CSS 1.3% Dockerfile 0.7%
4.2 KB · 116 lines python
Raw Blame History
1# Trouve-KA — scoreur Québec2# Author: Simon-Pierre Boucher3# Contact: contact@spboucher.ai45"""Calcul de page_quebec_score ∈ [0, 1].67Combinaison de signaux hétérogènes (CLAUDE.md §7) : TLD, toponymes, codes8postaux QC, indicatifs téléphoniques, organisations connues, langue française,9mentions structurées de la province. Aucun signal seul ne suffit (un .ca seul10ne prouve rien); le score sature progressivement via une somme amortie.11"""1213import re1415from trouveka.types import ParsedPage, QuebecSignals1617from .gazetteer import (18    AMBIGUOUS_TOPONYMS,19    QUEBEC_AREA_CODES,20    QUEBEC_ORGS,21    QUEBEC_TOPONYMS,22    STRONG_DOMAIN_SUFFIXES,23)2425# Codes postaux du Québec : G, H, J en première lettre (format A1A 1A1)26_POSTAL_RE = re.compile(r"\b[GHJ]\d[A-Z]\s?\d[A-Z]\d\b", re.IGNORECASE)27_PHONE_RE = re.compile(r"(?:\+?1[\s.-]?)?\(?(\d{3})\)?[\s.-]?\d{3}[\s.-]?\d{4}\b")28_PROVINCE_RE = re.compile(29    r"\b(?:province\s+(?:de\s+|du\s+)?qu[ée]bec|qu[ée]bec\s*\(qc\)|,\s*(?:qc|qu[ée]bec)\b)",30    re.IGNORECASE,31)32_WORD_BOUNDARY = r"(?<![\w-]){}(?![\w-])"333435def _count_terms(text: str, terms: set[str], cap: int = 10) -> tuple[int, list[str]]:36    found: list[str] = []37    total = 038    for term in terms:39        pattern = re.compile(_WORD_BOUNDARY.format(re.escape(term)), re.IGNORECASE)40        n = len(pattern.findall(text))41        if n:42            found.append(term)43            total += min(n, 4)  # une page qui répète 200× « Montréal » n'est pas 200× plus québécoise44        if total >= cap:45            break46    return min(total, cap), found474849def score_page(page: ParsedPage, domain: str) -> QuebecSignals:50    """Score Québec d'une page. Déterministe, sans LLM, économique (§12)."""51    signals = QuebecSignals()52    reasons: list[str] = []53    points = 0.05455    text = " ".join([page.title, page.description, " ".join(page.headings), page.body[:20_000]])56    text_with_hints = text + " " + " ".join(page.structured_hints)57    lower = text_with_hints.lower()5859    # 1. Domaine (signal fort mais pas suffisant seul)60    host = domain.lower()61    if any(host.endswith(suffix) or host == suffix.lstrip(".") for suffix in STRONG_DOMAIN_SUFFIXES):62        points += 4.063        reasons.append("tld_quebec")6465    # 2. Toponymes non ambigus (titre/headings pèsent plus que le corps)66    head_text = " ".join([page.title, page.description, " ".join(page.headings)])67    head_hits, head_names = _count_terms(head_text, QUEBEC_TOPONYMS, cap=6)68    body_hits, body_names = _count_terms(page.body[:20_000], QUEBEC_TOPONYMS, cap=8)69    if head_hits:70        points += 1.2 * head_hits71        reasons.append("toponymes_titre")72    if body_hits:73        points += 0.4 * body_hits74        reasons.append("toponymes_corps")75    signals.locations = sorted({*head_names, *body_names})[:12]7677    # 3. Toponymes ambigus — poids réduit78    amb_hits, amb_names = _count_terms(lower, AMBIGUOUS_TOPONYMS, cap=3)79    if amb_hits:80        points += 0.15 * amb_hits81        reasons.append("toponymes_ambigus")82        signals.locations = sorted({*signals.locations, *amb_names})[:12]8384    # 4. Codes postaux QC (signal fort : preuve d'adresse physique)85    postal_hits = len(set(_POSTAL_RE.findall(text_with_hints)))86    if postal_hits:87        points += min(postal_hits, 3) * 1.588        reasons.append("code_postal_qc")8990    # 5. Mention structurée de la province (adresses, footers)91    if _PROVINCE_RE.search(text_with_hints):92        points += 1.593        reasons.append("province_quebec")9495    # 6. Indicatifs téléphoniques (signal faible)96    area_codes = {m for m in _PHONE_RE.findall(text_with_hints) if m in QUEBEC_AREA_CODES}97    if area_codes:98        points += min(len(area_codes), 2) * 0.699        reasons.append("indicatif_qc")100101    # 7. Organisations québécoises connues102    org_hits, _ = _count_terms(lower, QUEBEC_ORGS, cap=6)103    if org_hits:104        points += 0.8 * org_hits105        reasons.append("organisations_qc")106107    # 8. Langue : le français augmente la probabilité sans la prouver108    if page.language == "fr":109        points += 0.8110        reasons.append("francais")111112    # Saturation douce : 0 pt → 0, ~3 pts → 0.5, ≥9 pts → ~0.95113    signals.score = round(points / (points + 3.0), 4) if points > 0 else 0.0114    signals.reasons = reasons115    return signals116