v1.12.0: max-2-stelsel — enkele familie-hit kan nooit solo doorlaten

- Gewicht-3-tier vervallen: alle families wegen max 2; drempel blijft 3,
  dus elke echte OV-bericht combineert met een tweede signaal (besluit
  2026-09-12, gevalideerd op 205 productie-artikelen).
- 'hermes' uit vervoerders + whitelist -> eigen zwakke familie (achternaam
  Ton Hermes, Margraten false positive 2026-09-12).
- Whitelist geen auto-pass meer: titel-hit = +2 titel-anker (onmiskenbare
  termen behouden hun recall; samenvatting blijft inert).
- Completheids-audit (2026-09-12): +20 termen/frasen —
  sterk: spoorsector, spoorbeheerder, spoornet, spoorvervoerder,
  treinvertraging/hinder/uitval/verstoring, ertms, teso, rangeer/rangeren,
  ice-<stad>/ice-trein, maaslijn/nedersaksenlijn/kamperlijn, ns international,
  deutsche bahn, european sleeper, italo, nightjet;
  zwak: busongeluk, busvervoer, spoor, staking, passagier(+reizigers),
  vervoer, vervoersbedrijf, vervoerregio, ritten, bovenleiding, abonnement.
- validate_filter.py: invariant gewicht in {1,2}, 46 dubbelzinnige + 39
  onmiskenbare termen, 52 regressie-cases groen.
This commit is contained in:
Eddy de Vink 2026-09-12 19:53:41 +02:00
commit 7997d7572a
3 changed files with 223 additions and 145 deletions

View file

@ -57,21 +57,21 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
# of het woord in beide velden = één signaal — 'busstation' 60× weegt 1×, besluit
# 2026-09-11). Drempel (3): doorlaten als de som van de unieke familie-gewichten
# >= MATCH_FILTER_THRESHOLD (titel weegt niet extra).
# Gewicht 3 (sterk, onmiskenbaar OV): één woord in titel óf samenvatting volstaat.
# Gewicht 2 (medium): moet combineren met een andere OV-familie; één medium-hit,
# in titel óf samenvatting, haalt de drempel niet.
# Gewicht 1 (zwak/dubbelzinnig: los "bus", "breng", "overweg", "overwegen", "sein",
# "kaartje", "opwaarderen", "rails", "pendelbus"): moet met een ander OV-woord
# combineren (een tweede, andere familie — dezelfde zwakke familie in titel én
# samenvatting telt niet als combinatie) EN minstens één zwak woord moet in de
# TITEL staan: een combinatie van alleen zwakke woorden in de samenvatting is
# geen bewijs (het golfbericht met "station" + "pendelbus" + "bus" in de
# samenvatting is geen OV-nieuws). Weert zo "VW-bus"/"schoolbus", "weer op de rails", het
# werkwoord "brengen" ("hoe breng je ... in beeld"), het idioom "goed met elkaar
# overweg kunnen", het werkwoord "overwegen" ("gratieverlening overwegen"), het
# werkwoord "opwaarderen" ("een wijk opwaarderen") en brandweerjargon
# ("sein brandmeester"), zonder eenduidige bushalte/overweg/NS/trein-berichten
# te raken.
# MAX-GEWICHT 2 — besluit 2026-09-12: een enkele FAMILIE-hit kan nooit solo
# doorlaten (2 < 3); écht OV-nieuws combineert in de praktijk altijd met een
# tweede signaal (gevalideerd op 205 productie-artikelen; zie beleidsblok).
# Gewicht 2 (onmiskenbaar OV: merken, sectorjargon, samenstellingen als bushalte/
# spoorlijn/veerboot — de vroegere 'sterk(3)' én 'medium(2)' zijn hierin
# opgegaan, want beide moeten nu combineren).
# Gewicht 1 (zwak/dubbelzinnig): moet met een andere OV-familie combineren
# (een tweede, andere familie — dezelfde zwakke familie in titel én samenvatting
# telt niet als combinatie) EN minstens één zwak woord moet in de TITEL staan:
# een combinatie van alleen zwakke woorden in de samenvatting is geen bewijs
# (het golfbericht met "station" + "pendelbus" + "bus" in de samenvatting is
# geen OV-nieuws). Weert zo "VW-bus"/"schoolbus", "weer op de rails", het
# werkwoord "brengen", het idioom "goed met elkaar overweg kunnen", het werkwoord
# "overwegen", het werkwoord "opwaarderen" en brandweerjargon ("sein brandmeester"),
# zonder eenduidige bushalte/overweg/NS/trein-berichten te raken.
# Inflectie: families matchen met \w* aan het eind, zodat meervoud en vervoeging
# meegenomen worden ("spoorlijnen", "seinstoringen", "ontspoorde", "chipkaarten").
# Een koppelteken vóór een match is géén OV-signaal: "VW-bus" en "radio-station"
@ -80,61 +80,69 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
# straatnamen rond stations — "de Stationsstraat afsluiten voor een festival"
# is geen OV-nieuws). Zwartgelijste woorden leveren geen score op, maar
# blokkeren de familie niet: een écht "station"-woord ernaast scoort gewoon.
# Bewust geaccepteerde botsingen: krant "De Metro"/café "De Tram" (medium).
# 'spoorbrug' is MEDIUM (niet sterk/whitelist): een brug is vaak louter locatie
# ('bij de spoorbrug tussen Liempde en Boxtel', Stuifmail 2026-09-12) — één
# spoorbrug-hit in de samenvatting is geen OV-nieuws; écht spoorbrug-nieuws
# combineert met trein/spoor/verkeer en haalt de drempel alsnog (besluit 2026-09-12).
# Losse algemene woorden zijn zwak (moeten combineren met een andere familie):
# "trein", "sprinter" (atleet), "vervoerder" (logistiek), "reiziger",
# "inchecken"/"uitchecken" (Schiphol/hotel), "station(s)" (omroepen), "bus",
# "sein" (brandweer), "overweg", "breng", "kaartje", "opwaarderen", "rails".
# Bewust geaccepteerde botsingen: krant "De Metro"/café "De Tram" (gewicht 2,
# moet combineren). 'spoorbrug' weegt 2: een brug is vaak louter locatie
# ('bij de spoorbrug tussen Liempde en Boxtel', Stuifmail 2026-09-12) — écht
# spoorbrug-nieuws combineert met trein/spoor/verkeer (besluit 2026-09-12).
# Naam-botsingen die bewust zijn afgezwakt (2026-09-12): 'hermes' (vervoerder
# Hermes vs. achternaam Ton Hermes — Margraten-false positive), 'breng'
# (vervoerder vs. werkwoord 'brengen'), 'spoor' (figuurlijk 'op het spoor van').
# Losse algemene woorden zijn zwak (moeten combineren): "trein", "sprinter"
# (atleet), "vervoerder" (logistiek), "reiziger"/"passagier" (vliegen),
# "inchecken"/"uitchecken" (Schiphol/hotel), "station(s)" (omroepen), "bus",
# "sein" (brandweer), "overweg", "breng", "kaartje", "opwaarderen", "rails",
# "staking" (alle sectoren), "vervoer" (vracht/auto), "abonnement", "ritten".
# Multi-woord-frasen en merknamen (ice-berlijn, deutsche bahn, european sleeper,
# ns international, teso, italo, nightjet, maaslijn) zijn onmiskenbaar OV en
# wegen 2; een los 'ice' is NIET toevoegbaar (case-insensitive botsing met 'ijs',
# besluit 2026-09-12).
# =====================================================================
# FILTERCLASSIFICATIE-BELEID (bij elke aanpassing aan het filter volgen)
# =====================================================================
# Doel: precisie boven recall. Liever een OV-bericht te veel gefilterd dan
# een niet-OV-bericht te veel doorgelaten (gebruikersbesluit, 2026-09-10).
#
# Drie lagen, elk met een eigen regel:
# 1. WHITELIST -> ALTIJD door BIJ EEN HIT IN DE TITEL. Alleen termen die
# in élke context OV betekenen (merken: NS/ProRail/Arriva; onmiskenbaar jargon:
# spoorstoring, perron, bushalte, conducteur, ovpay; multi-woord termen).
# REGEL: twijfel je of de term ook buiten het OV kan voorkomen -> NIET hier.
# NIET: station-samenstellingen (busstation/metrostation) — een bericht over
# een pand 'naast het busstation' is geen OV-nieuws (besluit 2026-09-11);
# die wegen mee via de gewogen families (medium).
# Een whitelist-term ALLEEN in de SAMENVATTING laat NIET onvoorwaardelijk
# door: een bijzin als 'de zakelijke bezoeker is ook nodig voor het openbaar
# vervoer' in een economiebericht is geen OV-nieuws (besluit 2026-09-12).
# Zo'n hit scoort via de gewogen families (dezelfde termen staan daar ook in).
# 2. GEWOGEN FAMILIES -> score = som van unieke OV-families over het hele artikel
# (titel + samenvatting; per familie 1× per artikel — herhaling of beide velden
# tellen niet dubbel, besluit 2026-09-11) >= drempel (3).
# - sterk (3): onmiskenbaar OV-jargon/merken/samenvoegingen (spoorlijn,
# bushalte, busverkeer, intercity, lijn 12, ...).
# - medium (2): onmiskenbaar OV maar iets algemener (tram, metro, halte,
# pont, ovpay, openbaar vervoer, trein-/bus-/metrostation).
# Moet COMBINEREN met een andere OV-familie: één medium-hit,
# in titel óf samenvatting, is onvoldoende — de titel weegt
# niet extra ('restaurant naast busstation' in de titel is
# geen OV-bericht, besluit 2026-09-11).
# EXTRA (besluit 2026-09-12): een medium + zwak-combinatie
# UITSLUITEND in de samenvatting (geen enkele hit in de
# titel) is geen OV-bewijs — 'openbaar vervoer' + 'zakelijke
# reizigers' in een economiebericht telt dan toch op tot de
# drempel. Sterke (3) families blijven in de samenvatting
# gewoon tellen.
# Lagen, elk met een eigen regel (MAX-2-stelsel, besluit 2026-09-12):
# 1. WHITELIST -> termen die in élke context OV betekenen (merken:
# NS/ProRail/Arriva; onmiskenbaar jargon: spoorstoring, perron, bushalte,
# conducteur, ovpay; multi-woord-frasen: nederlandse spoorwegen, openbaar
# vervoer, ice-berlijn ...). Een whitelist-hit in de TITEL telt +2 (geen
# auto-pass meer). Omdat elke whitelist-term óók in een familie zit, is dat
# effectief het oude titel-anker voor onmiskenbare termen — 'Reactie
# Nederlandse Spoorwegen' blijft daardoor door. REGEL: twijfel je of de term
# ook buiten het OV kan voorkomen -> NIET hier, maar zwak (eigen familie).
# Een whitelist-term ALLEEN in de SAMENVATTING blijft inert: een bijzin als
# 'de zakelijke bezoeker is ook nodig voor het openbaar vervoer' in een
# economiebericht is geen OV-nieuws (besluit 2026-09-12); zo'n hit scoort
# alleen via de overeenkomstige familie.
# 2. GEWOGEN FAMILIES -> score = som van unieke OV-families over het hele
# artikel (titel + samenvatting; per familie 1× per artikel — herhaling of
# beide velden tellen niet dubbel, besluit 2026-09-11) >= drempel (3).
# - sterk (2): onmiskenbaar OV-jargon/merken/samenvoegingen (spoorlijn,
# bushalte, busverkeer, intercity, veerboot, ertms,
# rangeer..., ice-frasen, ...). Eén hit (titel óf samenvatting)
# is onvoldoende: moet combineren (2 + 1 = 3). De vroegere
# 'één sterk woord volstaat'-regel is vervallen — 'hermes'
# als achternaam passeerde zo op één vervoerder-hit
# (Margraten, 2026-09-12).
# EXTRA (besluit 2026-09-12): een sterk/medium + zwak-
# combinatie UITSLUITEND in de samenvatting (geen enkele hit
# in de titel én geen whitelist-term in de titel) is geen
# OV-bewijs — 'openbaar vervoer' + 'zakelijke reizigers' in
# een economiebericht telt anders op tot de drempel.
# - zwak (1): DUBBELZINNIGHEID. Woorden die (ook) buiten het OV
# voorkomen. REGEL: elk dubbelzinnig woord krijgt een EIGEN
# familie (niet samengevoegd), zodat twee verschillende
# zwakke woorden wél als combinatie tellen, maar één enkel
# woord nooit de doorslag kan geven. Bekende dubbelzinnige:
# trein, bus, station(s), sprinter, reiziger, vervoerder,
# inchecken/uitchecken, sein, overweg, breng, kaartje,
# opwaarderen, rails, dienstregeling, chipkaart, ontsporen,
# pendelbus/shuttlebus (evenement-/hotel-/parkeerpendel;
# synoniemen -> bewust één familie, anders tellen twee
# synoniemen als twee onafhankelijke signalen).
# trein, bus, station(s), sprinter, reiziger/passagier,
# vervoerder, inchecken/uitchecken, sein, overweg, breng,
# kaartje, opwaarderen, rails, dienstregeling, chipkaart,
# ontsporen, pendelbus/shuttlebus (evenement-/hotel-/
# parkeerpendel; synoniemen -> bewust één familie),
# busongeluk/busvervoer (touringcar/schoolbus), staking,
# vervoer, vervoersbedrijf, vervoerregio, ritten,
# bovenleiding (industrie), abonnement, hermes, spoor.
# EXTRA REGEL voor zwak-alleen: staat er uitsluitend zwak
# bewijs in de SAMENVATTING (geen enkele zwakke hit in de
# titel), dan niet doorlaten. Twee/meer zwakke woorden in
@ -143,27 +151,38 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
# 3. BLACKLIST -> gematchte woorden zonder OV-waarde (stationsstraat,
# stationair, stationcar, ...); blokkeren de familie niet.
#
# BEOORDELEN VAN EEN NIEUW WOORD:
# BEOORDELEN VAN EEN NIEUW WOORD (incl. COMPLETEHEIDS-AUDIT, 2026-09-12):
# a) Kan het ook buiten het OV voorkomen? ja -> zwak (1), eigen familie.
# b) Is het een samenvoeging/merk die alleen OV kan betekenen? -> sterk (3)
# of whitelist.
# b) Is het een samenvoeging/merk die alleen OV kan betekenen? -> sterk (2)
# of whitelist (frasen die in élke context OV betekenen).
# c) Bij twijfel: zwak (1). Conservatief = precision-first.
# d) Ter controle: leg nieuwe woordkandidaten voor aan minimaal één extra
# taalmodel (de audit op 2026-09-10 gebruikte 3 modellen; enige stem
# 'TWIJFEL'/'NIET-OV' => behandelen als dubbelzinnig).
# e) COMPLETEHEID (besluit 2026-09-12): check bij elke filterwijziging óók of
# de families compleet zijn — draai de OV-gap-scan over de afgewezen
# artikelen van OV-specialist-feeds (SpoorPro/Treinreiziger/Mobiliteit),
# classificeer nieuwe kandidaten via stap a-d en vul aan. De audit van
# 2026-09-12 leverde o.a. spoorsector, spoorbeheerder, spoornet,
# spoorvervoerder, treinvertraging/uitval, ertms, teso, rangeer, ice-frasen,
# maaslijn, ns international, deutsche bahn, european sleeper, italo,
# nightjet (sterk) en 11 zwakke woorden op.
# =====================================================================
MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
# sterk (3)
(r"\b(?:ns|arriva|qbuzz|connexxion|cxx|transdev|keolis|ret|gvb|ebs|htm|syntus|u-ov|allgo|flixbus|blauwnet|r-net|valleilijn|waterbus|eurostar|thalys|randstadrail|gvu|prorail|hermes|rrreis|qliner|ov)\b", 3), # vervoerders ('Breng' staat in de zwakke groep: botst met het werkwoord "brengen")
(r"\b(?:spoorweg|spoorba(?:an|nen)|spoorlijn|spoorverkeer|spoorwegovergang|spoorstoring|perron|seinstoring|wisselstoring|spoorstaking)\w*\b", 3), # spoorinfra; let op de klinkerwisseling: 'ontsporing' (open lettergreep, één o) vs 'ontspoord'/'ontspoorde' (dicht, dubbel o) — vandaar beide stammen. Géén 'spoorbrug': die is medium (vaak locatie-referentie, besluit 2026-09-12)
(r"\b(?:treinstation|treinstations|metrostation|metrostations|busstation|busstations)\b", 2), # station-samenstellingen: onmiskenbaar OV, maar MEDIUM — één station-woord náást een ander onderwerp ('pand naast busstation', besluit 2026-09-11) is geen OV-bericht; in de samenvatting moet een tweede OV-woord bijkomen
# sterk (2) — onmiskenbaar OV, maar één hit is onvoldoende (max-2-stelsel,
# besluit 2026-09-12): elke echte OV-bericht combineert met een tweede signaal.
(r"\b(?:ns|arriva|qbuzz|connexxion|cxx|transdev|keolis|ret|gvb|ebs|htm|syntus|u-ov|allgo|flixbus|blauwnet|r-net|valleilijn|waterbus|eurostar|thalys|randstadrail|gvu|prorail|rrreis|qliner|ov|teso|deutsche bahn|european sleeper|ns international|italo|nightjet)\b", 2), # vervoerders; 'hermes' is zwak (achternaam-botsing, Margraten 2026-09-12), 'Breng' ook (werkwoord "brengen"); internationale vervoerders: teso (veer), deutsche bahn/european sleeper/italo/nightjet/ns international (audit 2026-09-12)
(r"\b(?:spoorweg|spoorba(?:an|nen)|spoorlijn|spoorverkeer|spoorwegovergang|spoorstoring|perron|seinstoring|wisselstoring|spoorstaking|spoorsector|spoorbeheerder|spoornet|spoorvervoerder|ertms)\w*\b", 2), # spoorinfra; let op de klinkerwisseling: 'ontsporing' (open lettergreep, één o) vs 'ontspoord'/'ontspoorde' (dicht, dubbel o) — vandaar beide stammen. Géén 'spoorbrug': die is 2 maar vaak louter locatie (besluit 2026-09-12). Uitbreiding 2026-09-12 (completheids-audit): spoorsector, spoorbeheerder, spoornet, spoorvervoerder, ertms
(r"\b(?:treinstation|treinstations|metrostation|metrostations|busstation|busstations)\b", 2), # station-samenstellingen: onmiskenbaar OV, maar één station-woord náást een ander onderwerp ('pand naast busstation', besluit 2026-09-11) is geen OV-bericht; in de samenvatting moet een tweede OV-woord bijkomen
(r"(?<![\w-])\bstations?\b", 1), # los 'station(s)' is ZWAK: ook tv-/radio-/regionale omroepstations ('de lokale stations'), moet combineren met een andere OV-familie
(r"\b(?:bus|tram|metro)-?(?:halte|haltes|lijn|lijnen|verkeer|net|staking|ba(?:an|nen)|hok(?:je|jes)|chauffeur)\w*\b", 3), # voertuig-samenstelling (bushalte/buslijn/bushokje/busbanen/…); géén 'station': *station-samenstellingen zitten in de station-familie, anders telde 'busstation' dubbel mee (via 'bus' én 'station') — fix 2026-09-11
(r"\b(?:treinstoring|treinverkeer|treinstaking|treinongeval|treinbotsing|treinongeluk)\w*\b", 3), # trein-jargon-samenstellingen: onmiskenbaar OV
(r"\b(?:bus|tram|metro)-?(?:halte|haltes|lijn|lijnen|verkeer|net|staking|ba(?:an|nen)|hok(?:je|jes)|chauffeur)\w*\b", 2), # voertuig-samenstelling (bushalte/buslijn/bushokje/busbanen/buschauffeur/…); géén 'station': *station-samenstellingen zitten in de station-familie, anders telde 'busstation' dubbel mee — fix 2026-09-11. Géén 'ongeluk'/'vervoer': die zijn bewust zwak (busongeluk = ook touringcar, busvervoer = ook bedrijfsvervoer, audit 2026-09-12)
(r"\b(?:treinstoring|treinverkeer|treinstaking|treinongeval|treinbotsing|treinongeluk|treinvertraging|treinhinder|treinuitval|treinverstoring)\w*\b", 2), # trein-jargon-samenstellingen: onmiskenbaar OV; uitgebreid met vertraging/hinder/uitval/verstoring (audit 2026-09-12)
(r"\b\w*trein\w*\b", 1), # losse 'trein' en algemene samenstellingen (stoptrein, goederentrein, treinverkeer al in bovenstaande familie) — ZWAK: 'de trein naar mijn nieuwe huis' is geen OV-jargon, moet combineren met een andere familie
(r"\b(?:intercity|streekvervoer|stadsvervoer|veerdienst|veerboot|veerboten|veerpont|voetveer|veerverbinding|buurtbus|belbus)\w*\b", 3), # treintypes, vervoerssectoren en veren ('sprinter' staat in de zwakke groep: atleet-botsing)
(r"\blijn\s?\d{1,3}\b", 3), # 'lijn 12' — vervoerslijn met nummer
# medium (2)
(r"\b(?:intercity|streekvervoer|stadsvervoer|veerdienst|veerboot|veerboten|veerpont|voetveer|veerverbinding|buurtbus|belbus|maaslijn|nedersaksenlijn|kamperlijn)\w*\b", 2), # treintypes, vervoerssectoren, veren en lijnnamen ('sprinter' staat in de zwakke groep: atleet-botsing); maaslijn/nedersaksenlijn/kamperlijn via audit 2026-09-12
(r"\blijn\s?\d{1,3}\b", 2), # 'lijn 12' — vervoerslijn met nummer
(r"(?:\brangeer\w*|\brangeren\w*)\b", 2), # rangeren/rangerend/rangeerterrein/rangeerbedrijf: onmiskenbaar spoorjargon (audit 2026-09-12; stam 'rangeren' = ranger+en, 'rangeerterrein' = rangeer+...)
(r"\bice[\s\-–—]*(?:berlijn|hamburg|brussel|bremen|parijs|frankfurt|keulen|amsterdam|bazel|milaan|kopenhagen|zurich|zürich|duitsland|international)\b|\bice-trein\w*\b", 2), # ICE-frasen: onmiskenbaar trein; los 'ice' is NIET toevoegbaar (case-insensitive botsing met 'ijs', besluit 2026-09-12)
# onmiskenbaar OV maar iets algemener (2, moet ook combineren)
(r"\btrams?\b", 2),
(r"\bmetro(?:'?s)?\b", 2),
(r"\b(?:openbaar vervoer|lightrail|halte|bushokje|busbaan)\w*\b", 2),
@ -171,14 +190,14 @@ MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
(r"\b(?:ov-staking|conducteursstaking)\w*\b", 2),
(r"\bconduct(?:eur|eurs)\b", 2), # trein-/buspersoneel: onmiskenbaar OV-jargon
(r"\b(?:pont|ponten)\b", 2), # waterbus/veerdienst
(r"\bspoorbrug(?:gen)?\b", 2), # spoorinfra, maar MEDIUM: vaak locatie-referentie, geen OV-nieuws ('bij de spoorbrug', Stuifmail 2026-09-12); moet combineren — zelfde regel als station-samenstellingen (besluit 2026-09-11)
(r"\bspoorbrug(?:gen)?\b", 2), # spoorinfra, maar vaak locatie-referentie ('bij de spoorbrug', Stuifmail 2026-09-12); moet combineren — zelfde regel als station-samenstellingen
# zwak (1) — dubbelzinnig, moet combineren
(r"(?<!-)\bbus(?:sen)?\b", 1), # ná een koppelteken geen OV: 'VW-bus', 'school-bus'
(r"\b(?:pendelbus|shuttlebus)\w*\b", 1), # evenement-/hotel-/parkeerpendel ('pendelbus naar de golfbaan') is geen OV; synoniemen bewust in één familie
(r"\b(?:inchecken|uitchecken)\w*\b", 1), # ook op luchthavens/hotels — geen OV-jargon, moet combineren
(r"\bsprinters?\b", 1), # ook atleet ('sprinter verpulvert record') — aparte familie, moet combineren
(r"\bvervoerders?\b", 1), # ook logistiek ('vrachtvervoerder') — aparte familie, moet combineren
(r"\breizigers?\b", 1), # ook algemeen ('zakenreizigers vliegen') — aparte familie, moet combineren
(r"\b(?:reizigers?|passagiers?)\b", 1), # ook 'zakenreizigers vliegen' / vliegtuigpassagiers — synoniemen bewust één familie (audit 2026-09-12)
(r"\bdienstregeling\w*\b", 1), # dubbelzinnig (modelaudit) — meestal OV, maar moet combineren
(r"\bchipkaart(?:en)?\b", 1), # dubbelzinnig (bankpas/algemeen) — aparte familie
(r"\breisproduct(?:en)?\b", 1), # dubbelzinnig — aparte familie
@ -191,9 +210,24 @@ MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
(r"\bkaart(?:je|jes)\b", 1),
(r"\bopwaarderen\b", 1), # ov-chipkaart opladen vs werkwoord "een wijk/park opwaarderen"
(r"\brails\b", 1),
(r"\bbusongeluk(?:ken)?\b", 1), # audit 2026-09-12: ook touringcar/schoolbus; combineert met buschauffeur/bus → door (busongeluk Zwitserland)
(r"\bbusvervoer\b", 1), # audit 2026-09-12: ook touringcar-/bedrijfsvervoer
(r"\bspoor\b", 1), # figuurlijk 'op het spoor van de daders', 'het spoor bijster'; écht spoornieuws combineert met spoorlijn/trein/perron
(r"\bstaking(?:en)?\b", 1), # arbeidsstaking in alle sectoren; 'spoorstaking'/'busstaking'/'ov-staking' zitten in de sterke families (besluit 2026-09-12)
(r"\bvervoer\b", 1), # ook vracht-/autovervoer; 'openbaar vervoer' zit in de sterke familie; samenstellingen (streekvervoer e.d.) matchen hier niet (geen \b)
(r"\bvervoersbedrijf(?:ven)?\b", 1), # ook goederen-/taxibedrijf (audit 2026-09-12)
(r"\bvervoerregio'?s?\b", 1), # OV-autoriteit ('Vervoerregio Amsterdam'), maar audit: ook algemener vervoersgebied
(r"\britten\b", 1), # ook fiets-/taxiritten (audit 2026-09-12)
(r"\bbovenleiding(?:en)?\b", 1), # audit 2026-09-12: ook water/gas/elektra-bovenleiding; spoor-combinatie (spoorlijn/ertms) haalt de drempel
(r"\babonnement(?:en)?\b", 1), # gym/museum/telefoon (audit 2026-09-12)
(r"\bhermes\b", 1), # vervoerder Hermes vs. achternaam (Ton Hermes, stichting Adoptie Graven Margraten, 2026-09-12)
]
# Drempel voor doorlaten (score = 2×titel + 1×samenvatting).
# Drempel voor doorlaten: som van unieke familie-gewichten >= 3. Max-2-stelsel
# (besluit 2026-09-12): geen enkele familie weegt meer dan 2, dus een enkele hit
# kan nooit solo doorlaten; een whitelist-term in de titel telt +2 (zie
# _filter_passes). De oude '2×titel + 1×samenvatting'-rekensom is vervallen
# (2026-09-11: de titel weegt niet extra).
MATCH_FILTER_THRESHOLD = 3
# Woorden die een familie wél matcht maar die geen OV-waarde hebben. Een hit dat
@ -216,34 +250,38 @@ MATCH_FILTER_BLACKLIST: frozenset[str] = frozenset({
"stationnement", "stationering",
})
# MATCH_FILTER_WHITELIST: termen die een bericht per definitie OV maken. Eén enkele
# hit in de TITEL laat ALTIJD door (geen familiescore nodig) — dit is het expliciete
# "onmiskenbaar OV"-signaal, zodat we voor deze termen niet op de gewogen
# heuristiek hoeven te leunen. Vergelijking is case-insensitive, dus ook 'nederlandse
# spoorwegen', 'prorail' en 'NS' in all-caps titels tellen mee.
# Een whitelist-hit ALLEEN in de samenvatting is géén onvoorwaardelijk signaal
# (besluit 2026-09-12: 'openbaar vervoer' als bijzin in een economiebericht is geen
# OV-nieuws); die term scoort dan via de gewogen families — voor whitelist-termen
# die geen eigen familie hebben (bv. 'treinstel': alleen zwak via \w*trein\w*) is
# een samenvatting-alleen hit dus zwak.
# Termen MOETEN in elke context OV betekenen. Niet hierop (algemeen/dubbelzinnig):
# los 'trein', 'bus', 'metro', 'tram', 'station', 'lijn', 'ov', 'reiziger',
# 'vervoerder', 'sprinter',
# 'inchecken' (Schiphol/hotel), 'sein' (brandweerjargon), 'overweg', 'treinramp'
# (films) — die blijven in de gewogen families.
# MATCH_FILTER_WHITELIST: termen die een bericht per definitie OV maken. Sinds
# besluit 2026-09-12 is dit geen auto-pass meer: een whitelist-hit in de TITEL
# telt +2 (één onmiskenbare term 2, nog een signaal nodig voor de drempel 3), en
# in de SAMENVATTING helemaal niet (die scoort via de overeenkomstige familie —
# een whitelist-term zit altijd óók in een familie). Zie _filter_passes.
# "Onmiskenbaar" = in élke context OV: merken (NS/ProRail/Arriva), onmiskenbaar
# jargon (spoorstoring, perron, bushalte, conducteur, ovpay), multi-woord-frasen
# (nederlandse spoorwegen, openbaar vervoer, ice-berlijn ...).
# Regel: twijfel je of de term ook buiten het OV kan voorkomen -> NIET hier, maar
# zwak (eigen familie). Daarom 'hermes' hieruit (achternaam, besluit 2026-09-12).
# 'treinstel'/'veerdienst'/'buurtbus'/'lijn 12' staan hier alleen als TITEL-anker
# (= het oude effect van één onmiskenbare term in de titel, nu via +2) — in de
# samenvatting wegen ze als familie (2/1). Vergelijking is case-insensitive.
MATCH_FILTER_WHITELIST: frozenset[str] = frozenset({
# Vervoerders en OV-merken (incl. multi-woord termen)
"nederlandse spoorwegen", "ns", "prorail", "randstadrail", "arriva", "qbuzz",
"connexxion", "transdev", "keolis", "ret", "gvb", "htm", "ebs", "syntus",
"u-ov", "allgo", "blauwnet", "r-net", "valleilijn", "hermes", "rrreis",
"u-ov", "allgo", "blauwnet", "r-net", "valleilijn", "rrreis",
"qliner", "flixbus", "eurostar", "thalys", "gvu", "ovpay",
"deutsche bahn", "european sleeper", "ns international", "italo", "nightjet",
"teso", "ice-berlijn",
# Spoorinfra-jargon (samenvoegingen, onmiskenbaar OV)
"spoorstoring", "spoorverkeer", "spoorwegovergang", "spoorlijn", "spoorbaan",
"spoorstaking", "perron", "wisselstoring", "seinstoring", "treinstoring", "treinverkeer",
"treinstaking", "treinongeval", "treinbotsing", "treinstel",
"spoorstaking", "spoorsector", "spoorbeheerder", "spoornet", "spoorvervoerder",
"ertms", "perron", "wisselstoring", "seinstoring", "treinstoring", "treinverkeer",
"treinstaking", "treinongeval", "treinbotsing", "treinstel", "treinvertraging",
"treinhinder", "treinuitval", "treinverstoring", "maaslijn", "nedersaksenlijn",
"kamperlijn",
# Bus/tram/metro-jargon
"bushalte", "bushaltes", "busbaan", "buslijn", "busnet",
"busstaking", "tramhalte", "tramlijn", "tramnet", "tramverkeer",
"busstaking", "buurtbus", "veerdienst", "lijn 12",
"tramhalte", "tramlijn", "tramnet", "tramverkeer",
"metrolijn", "metroverkeer",
# Tarief/vervoerder-jargon
"ov-chipkaart", "ov-staking", "lightrail",
@ -420,54 +458,63 @@ class RSSBot(Plugin):
@staticmethod
def _filter_passes(title: str, summary: str) -> bool:
"""Gewogen familiescore: elke OV-familie telt 1× per artikel (titel + samenvatting
samen; herhaling of beide velden = één signaal). Doorlaten bij som >= drempel.
(titel weegt NIET extra — één medium-hit, 'restaurant naast busstation',
is geen OV-bewijs; besluit 2026-09-11).
"""Gewogen familiescore (max-2-stelsel, besluit 2026-09-12): elke OV-familie
telt 1× per artikel (titel + samenvatting samen; herhaling of beide velden =
één signaal). Doorlaten bij som >= drempel (3). Geen enkele familie weegt
meer dan 2, dus een enkele familie-hit kan nooit solo doorlaten — 'hermes'
als achternaam passeerde vroeger op één vervoerder-hit van 3 (Margraten,
2026-09-12); écht OV-nieuws combineert in de praktijk altijd met een tweede
signaal (gevalideerd op 205 productie-artikelen).
Whitelist-termen (onmiskenbaar OV in élke context) tellen in de TITEL +2 —
géén auto-pass meer; omdat elke whitelist-term óók in een familie zit, is
dat effectief het behouden titel-anker ('Reactie Nederlandse Spoorwegen',
'Werkzaamheden aan perron', 'Buurtbus rijdt niet meer' blijven door). Een
whitelist-term ALLEEN in de samenvatting is geen OV-bewijs (besluit
2026-09-12: 'openbaar vervoer' als bijzin in een economiebericht) en scoort
alleen via de overeenkomstige familie.
Sterke families (gewicht 3) halen de drempel al met één woord in titel óf
samenvatting. Medium (2) moet combineren met een andere OV-familie.
Zwakke woorden (losse 'bus', 'breng', 'overweg', 'overwegen', 'sein',
'opwaarderen', 'kaartje', 'rails', 'pendelbus') moeten combineren met een
ander OV-woord én minstens één zwak woord moet in de titel staan; dezelfde
zwakke familie in titel én samenvatting is géén combinatie. Whitelist-termen
(NS, perron, bushalte, openbaar vervoer, ...) laten alleen onvoorwaardelijk
door BIJ EEN HIT IN DE TITEL; in de samenvatting wegen ze mee als gewogen
familie. Een medium + zwak-combinatie UITSLUITEND in de samenvatting (geen
enkele hit in de titel) is geen OV-bewijs ('openbaar vervoer' + 'zakelijke
reizigers' in een economiebericht, besluit 2026-09-12). Weert oa
'VW-bus' (bus matcht niet ná een koppelteken), 'schoolbus', 'bussen rijden
om' in een woningbrand-lead, het werkwoord 'brengen', het idioom 'goed met
elkaar overweg kunnen', 'een wijk opwaarderen', brandweerjargon
'sein brandmeester', figuratief 'weer op de rails' en een golfverslag
waarin de samenvatting 'station' + 'pendelbus' + 'bus' noemt.
Bewust doorgelaten: krant 'De Metro'/café 'De Tram' (medium) en atleet-
'sprinter' in combinatie met ander OV-woord."""
# Whitelist: een onmiskenbare OV-term in de TITEL laat altijd door (het
# artikel kondigt het onderwerp aan: 'NS verlengt noodregeling'). Een
# whitelist-term ALLEEN in de samenvatting is geen OV-bewijs ('openbaar
# vervoer' als bijzin in een economiebericht, besluit 2026-09-12) en
# scoort via de gewogen families hieronder.
if RSSBot._whitelist_hit(title):
return True
'opwaarderen', 'kaartje', 'rails', 'pendelbus', 'spoor', 'staking',
'vervoer', 'busongeluk', ...) moeten combineren met een ander OV-woord én
minstens één zwak woord moet in de titel staan (of een whitelist-term in de
titel); dezelfde zwakke familie in titel én samenvatting is géén combinatie.
Een sterk (2) + zwak-combinatie UITSLUITEND in de samenvatting (geen
familie-hit én geen whitelist-term in de titel) is geen OV-bewijs
('openbaar vervoer' + 'zakelijke reizigers' in een economiebericht, besluit
2026-09-12). Weert oa 'VW-bus' (bus matcht niet ná een koppelteken),
'schoolbus', 'bussen rijden om' in een woningbrand-lead, het werkwoord
'brengen', het idioom 'goed met elkaar overweg kunnen', 'een wijk
opwaarderen', brandweerjargon 'sein brandmeester', figuratief 'weer op de
rails', een golfverslag waarin de samenvatting 'station' + 'pendelbus' +
'bus' noemt, en het Margraten-bericht met 'Ton Hermes' als achternaam.
Bewust doorgelaten: krant 'De Metro'/café 'De Tram' (2, combineert) en
atleet-'sprinter' in combinatie met ander OV-woord."""
t_hits = RSSBot._fam_hits(title)
s_hits = RSSBot._fam_hits(summary)
# Eén familie telt 1× per ARTIKEL (titel én samenvatting samen): 'busstation'
# in beide velden, of 60× in de tekst, blijft één signaal (besluit 2026-09-11).
combined = dict(t_hits + s_hits)
score = sum(combined.values())
whitelist_title = RSSBot._whitelist_hit(title)
if whitelist_title:
# Whitelist-term in de titel = +2 (geen auto-pass, max-2-stelsel
# 2026-09-12): houdt het titel-anker voor onmiskenbare termen, maar
# een enkele whitelist-hit laat nooit meer solo door.
score += 2
if score < MATCH_FILTER_THRESHOLD:
return False
if max(combined.values()) == 1:
if max(combined.values(), default=0) == 1:
# Alleen zwakke families: dan moeten ≥2 verschillende families meetellen
# én minstens één zwak woord moet in de TITEL staan. Zwakke woorden in
# de samenvatting alleen zijn geen bewijs ('station' + 'pendelbus' +
# 'bus' in een golfverslag telt anders op tot de drempel).
if not t_hits or len(combined) < 2:
# én een titel-anker (familie-hit óf whitelist-term in de titel).
# Zwakke woorden in de samenvatting alleen zijn geen bewijs
# ('station' + 'pendelbus' + 'bus' in een golfverslag telt anders op
# tot de drempel).
if not whitelist_title and (not t_hits or len(combined) < 2):
return False
elif not t_hits and 1 in combined.values():
# Medium + zwak uitsluitend in de samenvatting, niets in de titel: geen
# OV-bewijs. 'openbaar vervoer' + 'zakelijke reizigers' in een
elif not t_hits and not whitelist_title and 1 in combined.values():
# Sterk (2) + zwak uitsluitend in de samenvatting, niets in de titel:
# geen OV-bewijs. 'openbaar vervoer' + 'zakelijke reizigers' in een
# economiebericht telt anders op tot de drempel (besluit 2026-09-12).
return False
return True
@ -476,20 +523,21 @@ class RSSBot(Plugin):
def _filter_summary() -> str:
"""Mensvriendelijke weergave van het vaste gewogen match-filter."""
return (
"**OV-matchfilter (vast in code, gewogen families):**\n"
"- Sterk (3): vervoerders (NS, Arriva, ProRail, Qbuzz, ...), spoorinfra (spoorlijn, spoorwegovergang, perron, spoorstoring), "
"samenstellingen als bushalte/buslijn/bushokje/busbaan/busverkeer, treinstoring/treinverkeer/treinstaking, "
"intercity/streekvervoer/veerdienst/buurtbus/belbus, 'lijn 12'\n"
"- Medium (2): tram, metro, openbaar vervoer, lightrail, halte, pont, spoorbrug, ov-chipkaart/ovpay, ov-staking, conducteur, "
"trein-/bus-/metrostation — één medium-hit volstaat niet, moet combineren met een andere OV-familie; "
"medium + zwak uitsluitend in de samenvatting (geen hit in de titel) telt niet\n"
"**OV-matchfilter (vast in code, gewogen families, max-2):**\n"
"- Sterk (2): vervoerders (NS, Arriva, ProRail, Qbuzz, Deutsche Bahn, TESO, ...), spoorinfra (spoorlijn, spoorstoring, perron, spoorsector, ertms), "
"samenstellingen als bushalte/buslijn/buschauffeur, treinstoring/treinverkeer/treinvertraging, "
"intercity/streekvervoer/veerdienst/buurtbus/belbus, 'lijn 12', ICE-frasen, rangeer... — één hit volstaat NIET: "
"moet combineren (max-2-stelsel, besluit 2026-09-12)\n"
"- Middel (2): tram, metro, openbaar vervoer, lightrail, halte, pont, spoorbrug, ov-chipkaart/ovpay, ov-staking, conducteur, "
"trein-/bus-/metrostation — zelfde regel: moet combineren met een andere OV-familie; "
"sterk/middel + zwak uitsluitend in de samenvatting (geen hit in de titel) telt niet\n"
"- Zwak (1): losse 'bus' (niet ná een koppelteken), 'pendelbus'/'shuttlebus', 'trein', 'station(s)', 'breng', 'overweg', 'overwegen', "
"'sein', 'kaartje', 'opwaarderen', 'rails', 'inchecken'/'uitchecken', 'dienstregeling', 'chipkaart', 'reisproduct', 'vervoersbewijs', "
"'ontsporen', 'reiziger', 'vervoerder', 'sprinter' — moet combineren met een ander OV-woord, met minstens één zwak woord in de titel\n\n"
"**Whitelist (altijd door bij een hit in de TITEL):** een onmisbare OV-term in de titel is genoeg "
"'ontsporen', 'reiziger'/'passagier', 'vervoerder', 'sprinter', 'spoor', 'staking', 'vervoer', 'busongeluk', ... — moet combineren met een ander OV-woord, met minstens één zwak woord in de titel\n\n"
"**Whitelist (+2 bij een hit in de TITEL, geen auto-pass):** onmiskenbare OV-termen "
"(NS, ProRail, Nederlandse Spoorwegen, spoorstoring, perron, bushalte, conducteur, "
"ov-chipkaart, ovpay, openbaar vervoer, ...); in de samenvatting alleen weegt zo'n term mee "
"als gewogen familie (medium/sterk)\n\n"
"ov-chipkaart, ovpay, openbaar vervoer, Deutsche Bahn, ICE Berlijn, ...); in de samenvatting alleen weegt zo'n term mee "
"als gewogen familie\n\n"
"Uitgesloten (blacklist): woorden die wél matchen maar geen OV-waarde hebben, "
"o.a. straatnamen rond stations ('stationsstraat', 'stationsweg', ...) en niet-OV-vormen als 'stationcar' en 'stationair'\n\n"
"Doorlaten: som van unieke OV-families ≥ 3 — één familie telt 1× per artikel "