v1.15.0: filter vereenvoudigd naar drie regels — 30 semantische groepen i.p.v. families+whitelist+blacklist
- Strip de filter terug tot: (1) sterk signaal in de titel -> door; (2) score van unieke semantische groepen >= 3 met titel-signaal -> door; (3) anders weg. Geen whitelist (82 termen), geen blacklist, geen span-merge, geen dienststoring-titelanker meer; woordgrenzen/lookbehinds weren de bekende botsingen structureel (schoolbus/VW-bus/stationsstraat/stationair/ treinstraat/treinweg). - Lijsten 49 families + 82 whitelist + 2 blacklist = 133 -> 16 sterk + 14 ambigu = 30 groepen; filterzone 524 -> ~292 regels; match-methodes 4 -> 2. - Vijf historische uitzonderings-families geschrapt (busongeluk, sein, breng, hermes, inchecken, kaartje, rails, staking, e.d.) — die woorden leveren nu 0 signalen op en de bekende false positives blijven geweerd. - Completheids-audit ving de Maaslijn/Nedersaksenlijn/Kamperlijn/waterbus- regressie op (hersteld); resterende zero-hit-kandidaten geen wijziging. - validate_filter.py: invarianten 1-3 + 113 regressie-cases (90 onveranderd groen + 23 nieuw); prompt-matrix 27/27. - Gedragswijziging op 45 productiedagen: uitsluitend false positives extra geweerd (buitenlandse veerboot-rampen, Zwitsers busongeluk, delftopzondag tram+halte, Cox/RET, tram-blokkade); 0 recalls verloren. - README/AGENTS.md/vault bijgewerkt.
This commit is contained in:
parent
0c5a07071c
commit
ac30575660
6 changed files with 381 additions and 695 deletions
549
buzzrss/bot.py
549
buzzrss/bot.py
|
|
@ -52,283 +52,103 @@ from .migrations import upgrade_table
|
|||
rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STATE)
|
||||
|
||||
|
||||
# Gewogen OV-trefwoord-families voor het match-filter. Elke tuple = (regex, gewicht).
|
||||
# Per familie wordt 1× geteld per ARTIKEL (titel én samenvatting samen; herhaling
|
||||
# of het woord in beide velden = één signaal — 'busstation' 60× weegt 1×, besluit
|
||||
# 2026-09-11). Eén term die meerdere families matcht telt óók maar 1× (hoogste
|
||||
# gewicht wint): 'ov-chipkaart' bevat 'chipkaart', 'openbaar vervoer' bevat
|
||||
# 'vervoer', 'treinstoring' matcht de zwakke \w*trein\w*-familie mee — geen
|
||||
# dubbeltelling (besluit 2026-09-13; 'busbaan' stond in twee families en is
|
||||
# uit de halte-familie verwijderd, refactor 2026-09-13). Drempel (3): doorlaten
|
||||
# als de som van de unieke familie-gewichten >= MATCH_FILTER_THRESHOLD
|
||||
# (titel weegt niet extra).
|
||||
# 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"
|
||||
# vallen af (lookbehind bij 'bus' resp. 'station').
|
||||
# MATCH_FILTER_BLACKLIST: gematchte woorden die tóch geen OV-waarde hebben (o.a.
|
||||
# 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" (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).
|
||||
# OV-MATCHFILTER (v1.15) — precisie boven recall (gebruikersbesluit
|
||||
# 2026-09-10): liever een OV-bericht te veel wegfilteren dan één niet-OV-bericht
|
||||
# te veel plaatsen.
|
||||
#
|
||||
# Beslissing, in drie stappen (refactor 2026-09-13; logica equivalent met de
|
||||
# voorheen gelaagde takken — MAX-2-stelsel, besluit 2026-09-12):
|
||||
# 1. STERK SIGNAAL (whitelist-term óf dienststoring-zin in de TITEL) -> DOORLATEN.
|
||||
# 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,
|
||||
# bovenop het familie-gewicht van dezelfde term (elke whitelist-term zit
|
||||
# óók in een familie) — samen altijd >= drempel, dus eenduidige OV-titels
|
||||
# ('Reactie Nederlandse Spoorwegen', 'Werkzaamheden aan perron') passeren
|
||||
# alleen. 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 familie.
|
||||
# 2. CONTEXT: som van unieke OV-signalen over titel + samenvatting
|
||||
# >= drempel (3). Per familie 1× per artikel (herhaling of beide velden
|
||||
# tellen niet dubbel, besluit 2026-09-11); overlappende familie-treffers
|
||||
# van dezelfde term wegen 1× met het hoogste gewicht (span-merge in
|
||||
# _fam_hits, besluit 2026-09-13 — 'ov-chipkaart' bevat 'chipkaart' maar
|
||||
# weegt als één term 2, geen 3).
|
||||
# - sterk (2): onmiskenbaar OV-jargon/merken/samenvoegingen (spoorlijn,
|
||||
# bushalte, busverkeer, intercity, veerboot, ertms,
|
||||
# rangeer..., ice-frasen, ...) én het dienststoring-
|
||||
# contextpatroon 'vervoerswoord + rijdt/rijden (niet) +
|
||||
# niet' ('sprinters rijden niet', 'metro rijdt niet').
|
||||
# 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).
|
||||
# - 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/passagier,
|
||||
# vervoerder, inchecken/uitchecken, sein, overweg, breng,
|
||||
# kaartje, opwaarderen, rails, dienstregeling, chipkaart,
|
||||
# reisproduct, vervoer(s)bewijs, ontsporen,
|
||||
# pendelbus/shuttlebus (evenement-/hotel-/parkeerpendel;
|
||||
# synoniemen -> bewust één familie), busongeluk/busvervoer
|
||||
# (touringcar/schoolbus), staking, vervoer, vervoersbedrijf,
|
||||
# vervoerregio, ritten, bovenleiding (industrie),
|
||||
# abonnement, hermes, spoor.
|
||||
# 3. TITEL-ANKER: staat er geen enkel OV-signaal in de titel (geen
|
||||
# familie-hit, geen whitelist-term), dan is de score uitsluitend
|
||||
# samenvatting-bewijs en mag die geen zwak woord bevatten. Twee/meer
|
||||
# zwakke woorden in de samenvatting ('station' + 'pendelbus' + 'bus' in
|
||||
# een golfverslag) en sterk+zwak uitsluitend in de samenvatting
|
||||
# ('openbaar vervoer' + 'zakelijke reizigers' in een economiebericht,
|
||||
# beide besluiten 2026-09-12) zijn geen OV-bewijs. Alleen twee sterke
|
||||
# termen in de samenvatting blijven bewust door (delftopzondag
|
||||
# 'tram 1, halte Buitenhofdreef', besluit 2026-09-13).
|
||||
# BLACKLIST -> gematchte woorden zonder OV-waarde, restant van
|
||||
# straatnamen rond spoorlijnen die de zwakke \b\w*trein\w*\b-familie zou
|
||||
# matchen ('treinstraat'/'treinweg'); blokkeren de familie niet. De
|
||||
# oudere stations*-varianten zijn sinds de woordgrens-familie
|
||||
# \bstations?\b inert en verwijderd (2026-09-13) — gedrag vastgelegd in
|
||||
# validate_filter.py.
|
||||
# Beslissing, in drie regels:
|
||||
# 1. STERK signaal in de TITEL -> plaatsen
|
||||
# 2. score >= drempel (3) én de TITEL bevat minstens één signaal
|
||||
# -> plaatsen (context)
|
||||
# 3. anders -> wegfilteren
|
||||
#
|
||||
# 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 (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.
|
||||
# Dienststoring-contextpatroon: vervoerswoord + 'rijdt/rijden (niet)' + 'niet'
|
||||
# ('sprinters rijden niet', 'metro rijdt niet tussen Rotterdam en Schiedam').
|
||||
# Gedeeld door de familie én het titel-anker (_disruption_hit): in de TITEL is de
|
||||
# storingszin een sterk OV-signaal (net als een whitelist-term), in de samenvatting
|
||||
# weegt hij als familie (2). 'rijden' an sich is géén OV-signaal. 'schoolbus'
|
||||
# matcht niet ('bus' niet aan het woordbegin), refactor 2026-09-13.
|
||||
_DISRUPTION_RE_SRC = (
|
||||
r"\b(?:sprinters?|stoptrein(?:en)?|intercity'?s?|trein(?:en)?|trams?|bussen|bus|"
|
||||
r"metro'?s?|veerdienst(?:en)?)\s+(?:rijdt|rijden|reed|reden)\s+niet\b"
|
||||
)
|
||||
|
||||
# score = som van de gewichten van unieke SEMANTISCHE GROEPEN over titel +
|
||||
# samenvatting. Elke groep telt hooguit één keer per artikel — meerdere woorden
|
||||
# uit dezelfde groep ('bus' + 'buslijn' + 'busvervoer', of 'station' +
|
||||
# 'stationsgebouw' + 'stationsplein') leveren samen één signaal op en tellen
|
||||
# nooit kunstmatig op (refactor v1.15). Woordgrenzen en koppelteken-lookbehinds
|
||||
# weren de bekende botsingen structureel: 'schoolbus'/'VW-bus' matchen de
|
||||
# bus-groep niet, 'stationsstraat'/'stationair'/'stationcar'/'stationwagen' de
|
||||
# station-groep niet — er is GEEN zwarte lijst meer nodig.
|
||||
#
|
||||
# STERK (2 punten): termen die vrijwel altijd betekenen dat het artikel over
|
||||
# OV gaat; in de TITEL zijn ze zelfstandig voldoende ("een sterk signaal mag
|
||||
# zelfstandig voldoende zijn"). In de samenvatting tellen ze als context van 2.
|
||||
# Honkvaste typografie: 'ns' alleen op woordgrens (niet in 'kunstenaar'),
|
||||
# 'ov' niet in 'ov-chipkaart'/'ov-staking' ((?!-)-lookahead), de Meervouds-
|
||||
# vormen via (?:en)?-suffix.
|
||||
# AMBIGU (1 punt; halte/tram/metro/spoorbrug 2): contextafhankelijk en nooit
|
||||
# zelfstandig voldoende — 'bus', 'trein', 'station', 'tram', 'metro', 'halte',
|
||||
# 'reiziger', 'vervoerder', 'sprinter', 'pendelbus', 'spoorbrug',
|
||||
# 'dienstregeling', 'ontsporen', 'spoor'. Alleen combinaties tellen:
|
||||
# trein+spoorlijn, metro+rijdt-niet, tram+dienstregeling, sprinter+NS,
|
||||
# bus+halte. Dezelfde groep in titel én samenvatting blijft één signaal.
|
||||
# =====================================================================
|
||||
MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
|
||||
# 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", 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). 'busbaan'/'bushokje' stonden óók in de halte-familie — verwijderd bij de refactor 2026-09-13 (geen dubbelwoorden in twee families)
|
||||
(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|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)\w*\b", 2), # frasen en halte-hits; 'bushokje'/'busbaan' matchen al de bus-samenstelling-familie — bewust niet dubbel in twee families (refactor 2026-09-13)
|
||||
(r"\b(?:ov-chipkaart|ovpay|ov-pay|studentenreisproduct)\w*\b", 2),
|
||||
(r"\b(?:ov-staking|conducteursstaking)\w*\b", 2),
|
||||
(_DISRUPTION_RE_SRC, 2), # dienststoring-context ('sprinters rijden niet', 'metro rijdt niet...'); zie _DISRUPTION_RE_SRC
|
||||
(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 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"\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
|
||||
(r"\bvervoer(?:s)?bewijs\w*\b", 1), # dubbelzinnig — aparte familie
|
||||
(r"\b(?:ontspor|ontspoor)\w*\b", 1), # werkwoord 'ontsporen' is dubbelzinnig ('het plan ontspoorde'); echte trein-ontsporing combineert met trein/spoor
|
||||
(r"\bbreng\b", 1), # vervoerder Breng vs werkwoord "brengen" ("hoe breng je ... in beeld")
|
||||
(r"\boverweg\b", 1), # spoorwegovergang vs idioom "goed met elkaar overweg kunnen"
|
||||
(r"\boverwegen\b", 1), # meervoud van overweg vs werkwoord ("een gratieverlening overwegen")
|
||||
(r"\bsein\b", 1), # spoorsein vs brandweerjargon ("sein brandmeester") en figuurlijk gebruik
|
||||
(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)
|
||||
MATCH_FILTER_STRONG: list[tuple[str, str]] = [
|
||||
# vervoerders / OV-merken
|
||||
(r"\b(?:ns|prorail|arriva|qbuzz|connexxion|cxx|transdev|keolis|ret|gvb|htm|ebs"
|
||||
r"|syntus|u-ov|allgo|flixbus|blauwnet|r-net|valleilijn|gvu|rrreis|qliner|teso"
|
||||
r"|eurostar|thalys|randstadrail|deutsche bahn|ns international|european sleeper"
|
||||
r"|italo|nightjet)\b", "vervoerders"),
|
||||
(r"(?<!-)\bov\b(?!-)", "ov"), # 'het OV'; niet in ov-chipkaart/ov-staking
|
||||
(r"\b(?:spoorweg(?:en)?|spoorba(?:an|nen)|spoorlijn(?:en)?|spoorverkeer"
|
||||
r"|spoorwegovergang(?:en)?|spoorstoring(?:en)?|spoorstaking(?:en)?|spoorsector"
|
||||
r"|spoorbeheerder|spoornet|spoorvervoerder|wisselstoring(?:en)?|seinstoring(?:en)?"
|
||||
r"|ertms|treinstoring(?:en)?|treinverkeer|treinstaking(?:en)?|treinongeval(?:len)?"
|
||||
r"|treinbotsing(?:en)?|treinvertraging(?:en)?|treinhinder|treinuitval"
|
||||
r"|treinverstoring(?:en)?|treinstel(?:len)?)\b", "spoorjargon"),
|
||||
(r"\bperrons?\b", "perron"),
|
||||
(r"\b(?:bushaltes?|buslijn(?:en)?|busba(?:an|nen)|buschauffeurs?|bushokjes?"
|
||||
r"|busnet(?:ten)?|busverkeer|busstaking(?:en)?)\b", "bussamenstelling"),
|
||||
(r"\b(?:tramhaltes?|tramlijn(?:en)?|tramverkeer|tramnet(?:ten)?)\b", "tramsamenstelling"),
|
||||
(r"\b(?:metrolijn(?:en)?|metroverkeer)\b", "metrosamenstelling"),
|
||||
(r"\b(?:intercity'?s?|streekvervoer|stadsvervoer|buurtbus(?:sen)?|belbus(?:sen)?"
|
||||
r"|veerdienst(?:en)?|waterbus(?:sen)?)\b", "treintype"),
|
||||
(r"\b(?:maaslijn|nedersaksenlijn|kamperlijn)\b", "lijnnaam"), # OV-lijnnamen (audit 2026-09-12)
|
||||
(r"\bconducteurs?\b", "conducteur"),
|
||||
(r"\b(?:ov-chipkaart|ovpay|ov-pay|studentenreisproduct)\b", "ovtarief"),
|
||||
(r"\blightrail\b", "lightrail"),
|
||||
(r"\blijn\s?\d{1,3}\b", "lijnnummer"),
|
||||
(r"\b(?:rangeer\w*|rangeren)\b", "rangeren"),
|
||||
(r"\bopenbaar vervoer\b", "openbaar vervoer"),
|
||||
(r"\bice[\s\-–—]*(?:berlijn|hamburg|brussel|bremen|parijs|frankfurt|keulen"
|
||||
r"|amsterdam|bazel|milaan|kopenhagen|zurich|zürich|duitsland|international)\b"
|
||||
r"|\bice-trein(?:en)?\b", "ice"),
|
||||
# dienststoring: vervoerswoord + 'rijdt/rijden/vaart (niet)' + 'niet'
|
||||
(r"\b(?:sprinters?|stoptrein(?:en)?|intercity'?s?|trein(?:en)?|trams?|bussen|bus"
|
||||
r"|metro'?s?|veerdienst(?:en)?)\s+(?:rijdt|rijden|reed|reden|vaart|varen)"
|
||||
r"\s+niet\b", "dienststoring"),
|
||||
]
|
||||
MATCH_FILTER_AMBIGUE: list[tuple[str, int, str]] = [
|
||||
(r"\bhaltes?\b", 2, "halte"), # contextzwaar, niet zelfstandig
|
||||
(r"\btrams?\b", 2, "tram"),
|
||||
(r"\bmetro'?s?\b", 2, "metro"),
|
||||
(r"\bspoorbrug(?:gen)?\b", 2, "spoorbrug"), # vaak louter locatie (v1.10.1)
|
||||
(r"(?<!-)\bbus(?:sen)?\b", 1, "bus"), # 'VW-bus'/'schoolbus' matchen niet
|
||||
(r"\b\w*trein\w*\b", 1, "trein"), # trein, stoptrein, treinreizigers…
|
||||
(r"\bstations?\b|\b(?:trein|bus|metro)station(?:s)?\b", 1, "station"),
|
||||
(r"\breizigers?\b|\bpassagiers?\b", 1, "reiziger"),
|
||||
(r"(?<!openbaar )\bvervoer(?:der|ders|sbedrij(?:f|ven)|regio'?s?)?\b", 1, "vervoer"),
|
||||
(r"\bsprinters?\b", 1, "sprinter"),
|
||||
(r"\b(?:pendel|shuttle)bus(?:sen)?\b", 1, "pendel"), # evenement-/hotelpendel
|
||||
(r"\bdienstregeling\w*\b", 1, "dienstregeling"),
|
||||
(r"\b(?:ontspor(?:en|ing)?|ontspoord(?:e)?)\b", 1, "ontsporen"),
|
||||
(r"\bspoor\b", 1, "spoor"), # figuurlijk 'op het spoor van'
|
||||
]
|
||||
|
||||
# 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).
|
||||
# Drempel voor de context-regel (stap 2): som van unieke groeps-gewichten >= 3.
|
||||
# Max-gewICHT 2: een enkel signaal (zelfs halte/tram/metro/spoorbrug) kan nooit
|
||||
# solo doorlaten; écht OV-nieuws combineert in de praktijk altijd (gevalideerd
|
||||
# op 205 productie-artikelen, 2026-09-12).
|
||||
MATCH_FILTER_THRESHOLD = 3
|
||||
|
||||
# Woorden die een familie wél matcht maar die geen OV-waarde hebben. Een hit dat
|
||||
# hierin staat telt niet mee voor de score (en blokkeert de familie niet). Vorm:
|
||||
# exact woord (lowercase), zoals door de familie-regex wordt gematcht.
|
||||
# Refactor 2026-09-13: alleen woorden die een HUIDIGE familie nog matchen blijven
|
||||
# staan ('treinstraat'/'treinweg' via de zwakke \b\w*trein\w*\b-familie). De
|
||||
# stations*-varianten (stationsstraat, stationair, stationeren, stationcar,
|
||||
# stationwagen, stationsplein, ...) waren inert sinds de woordgrens-familie
|
||||
# \bstations?\b en zijn verwijderd — dat gedrag is in validate_filter.py
|
||||
# vastgelegd.
|
||||
MATCH_FILTER_BLACKLIST: frozenset[str] = frozenset({
|
||||
# Straatnamen rond spoorlijnen die de \b\w*trein\w*\b-familie zou matchen:
|
||||
# een straat die toevallig 'Trein' heet maakt een bericht geen OV-bericht.
|
||||
"treinstraat", "treinweg",
|
||||
})
|
||||
|
||||
# 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", "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", "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", "buurtbus", "veerdienst", "lijn 12",
|
||||
"tramhalte", "tramlijn", "tramnet", "tramverkeer",
|
||||
"metrolijn", "metroverkeer",
|
||||
# Tarief/vervoerder-jargon
|
||||
"ov-chipkaart", "ov-staking", "lightrail",
|
||||
"conducteur", "conducteurs",
|
||||
"openbaar vervoer",
|
||||
})
|
||||
|
||||
_compiled_match_families: list[tuple[re.Pattern, int]] = [
|
||||
(re.compile(pat, re.IGNORECASE), w) for pat, w in MATCH_FILTER_FAMILIES
|
||||
_compiled_match_strong: list[tuple[re.Pattern, str]] = [
|
||||
(re.compile(pat, re.IGNORECASE), name) for pat, name in MATCH_FILTER_STRONG
|
||||
]
|
||||
_compiled_match_ambigue: list[tuple[re.Pattern, int, str]] = [
|
||||
(re.compile(pat, re.IGNORECASE), w, name) for pat, w, name in MATCH_FILTER_AMBIGUE
|
||||
]
|
||||
|
||||
# Whitelist: enkel-woord termen als \\b-term\\b (geen substring-botsingen zoals
|
||||
# 'ns' in 'kunstenaar'), multi-woord termen als exacte frase.
|
||||
_WHITELIST_RE = re.compile(
|
||||
r"\b(?:" + "|".join(re.escape(w) for w in MATCH_FILTER_WHITELIST if " " not in w) + r")\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_WHITELIST_PHRASES: tuple[str, ...] = tuple(w for w in MATCH_FILTER_WHITELIST if " " in w)
|
||||
|
||||
# Dienststoring-context als sterk TITEL-signaal (zie _DISRUPTION_RE_SRC).
|
||||
_DISRUPTION_RE = re.compile(_DISRUPTION_RE_SRC, re.IGNORECASE)
|
||||
|
||||
|
||||
class Config(BaseProxyConfig):
|
||||
def do_update(self, helper: ConfigUpdateHelper) -> None:
|
||||
helper.copy("update_interval")
|
||||
|
|
@ -456,147 +276,84 @@ class RSSBot(Plugin):
|
|||
return re.sub(r"<[^>]+>", "", html.unescape(raw))
|
||||
|
||||
@staticmethod
|
||||
def _whitelist_hit(text: str) -> bool:
|
||||
"""True als een onmiskenbare OV-term uit MATCH_FILTER_WHITELIST in text zit.
|
||||
def _signals(text: str) -> dict[str, int]:
|
||||
"""Unieke semantische OV-groepen in text -> gewicht (sterk 2, ambigu 1/2).
|
||||
|
||||
Enkele-woord termen matchen op woordgrens ('ns' telt NIET binnen 'kunstenaar'),
|
||||
multi-woord termen ('nederlandse spoorwegen') als exacte frase, beide
|
||||
case-insensitive."""
|
||||
if _WHITELIST_RE.search(text):
|
||||
return True
|
||||
low = text.lower()
|
||||
return any(ph in low for ph in _WHITELIST_PHRASES)
|
||||
|
||||
@staticmethod
|
||||
def _disruption_hit(text: str) -> bool:
|
||||
"""True als een dienststoring-zin ('sprinters rijden niet', 'metro rijdt
|
||||
niet tussen Rotterdam en Schiedam') in text staat. In de TITEL telt zo'n
|
||||
zin als sterk OV-signaal (+2, net als een whitelist-term); als familie
|
||||
(2) scoort hij overal — zie _DISRUPTION_RE_SRC."""
|
||||
return _DISRUPTION_RE.search(text) is not None
|
||||
|
||||
@staticmethod
|
||||
def _fam_hits(text: str) -> list[tuple[int, int]]:
|
||||
"""(familie-index, gewicht) per OV-signaal in text (1× per familie).
|
||||
|
||||
Gematchte woorden uit MATCH_FILTER_BLACKLIST (o.a. 'stationsstraat')
|
||||
tellen niet mee en blokkeren de familie niet: een écht OV-woord in
|
||||
dezelfde tekst scoort gewoon verder.
|
||||
|
||||
Eén term telt maximaal één signaal (besluit 2026-09-13): overlappende
|
||||
treffers van verschillende families op dezelfde plek worden samengevoegd
|
||||
met het hoogste gewicht. Voorheen telde één woord dubbel als het in
|
||||
meerdere families stond — 'ov-chipkaart' bevat 'chipkaart' (2+1=3),
|
||||
'openbaar vervoer' bevat 'vervoer' (2+1=3), 'treinstoring' matcht ook
|
||||
de zwakke \\w*trein\\w*-familie (2+1=3). Max-2-stelsel: één term kan
|
||||
nooit solo doorlaten."""
|
||||
spans: list[tuple[int, int, int, int]] = [] # (start, end, familie-idx, gewicht)
|
||||
for idx, (family, weight) in enumerate(_compiled_match_families):
|
||||
for m in family.finditer(text):
|
||||
if m.group(0).lower() in MATCH_FILTER_BLACKLIST:
|
||||
continue
|
||||
spans.append((m.start(), m.end(), idx, weight))
|
||||
break
|
||||
spans.sort()
|
||||
merged: list[tuple[int, int, int, int]] = []
|
||||
for start, end, idx, weight in spans:
|
||||
if merged and start < merged[-1][1]:
|
||||
s0, e0, i0, w0 = merged[-1]
|
||||
if weight > w0:
|
||||
merged[-1] = (s0, max(e0, end), idx, weight)
|
||||
else:
|
||||
merged[-1] = (s0, max(e0, end), i0, w0)
|
||||
else:
|
||||
merged.append((start, end, idx, weight))
|
||||
return [(idx, w) for _, _, idx, w in merged]
|
||||
Elke groep telt hooguit één keer: meerdere woorden uit dezelfde groep
|
||||
('bus' + 'buslijn' + 'busvervoer', 'station' + 'stationsgebouw') leveren
|
||||
samen één signaal op; er is geen span-merge of zwarte lijst nodig omdat
|
||||
de groepen met woordgrenzen/lookbehinds overlappingsvrij zijn
|
||||
('schoolbus'/'VW-bus' matchen de bus-groep niet, 'stationsstraat'/
|
||||
'stationair' de station-groep niet, 'ov-chipkaart' niet de ov-groep)."""
|
||||
out: dict[str, int] = {}
|
||||
for family, name in _compiled_match_strong:
|
||||
if family.search(text):
|
||||
out[name] = 2
|
||||
for family, weight, name in _compiled_match_ambigue:
|
||||
if family.search(text):
|
||||
out[name] = max(out.get(name, 0), weight)
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _filter_passes(title: str, summary: str) -> bool:
|
||||
"""Driestaps-beslissing, precision-first (refactor 2026-09-13; beslissing
|
||||
equivalent met v1.13.0, maar de drie historische uitzonderings-takken
|
||||
reduceren tot één regel — zie stap 3):
|
||||
"""Beslissing in drie regels (v1.15, precision-first):
|
||||
|
||||
1. STERK SIGNAAL: een whitelist-term óf een dienststoring-zin
|
||||
('sprinters rijden niet') in de titel is voldoende. Elke
|
||||
whitelist-term staat óók in een familie, dus zo'n titel telt +2 én
|
||||
het familie-gewicht — samen altijd >= drempel ('NS meldt storing',
|
||||
'Werkzaamheden aan perron', 'Buurtbus rijdt niet meer'). In de
|
||||
samenvatting ALLEEN is zo'n term géén bewijs ('openbaar vervoer'
|
||||
als bijzin in een economiebericht, besluit 2026-09-12) en scoort
|
||||
die alleen via de overeenkomstige familie.
|
||||
1. STERK signaal in de TITEL -> plaatsen. Een sterk signaal (vervoerder,
|
||||
spoorjargon, perron, bushalte/buslijn, conducteur, ovpay, ...
|
||||
zie MATCH_FILTER_STRONG) vrijwel altijd over OV; in de titel is het
|
||||
zelfstandig voldoende ('NS meldt storing', 'Werkzaamheden aan
|
||||
perron', 'Sprinters rijden niet tussen Utrecht en Amsterdam').
|
||||
|
||||
2. CONTEXT: de som van unieke OV-signalen over titel + samenvatting
|
||||
>= drempel (3). Eén term telt één signaal per artikel ('busstation'
|
||||
60× of in beide velden = één keer); overlappende familie-treffers
|
||||
van dezelfde term wegen 1× met het hoogste gewicht (span-merge in
|
||||
_fam_hits, besluit 2026-09-13 — 'ov-chipkaart' bevat 'chipkaart'
|
||||
maar weegt als één term 2, geen 3). Geen
|
||||
familie weegt meer dan 2 (max-2-stelsel, besluit 2026-09-12): een
|
||||
enkel sterk woord kan nooit solo doorlaten ('hermes' als
|
||||
achternaam passeerde vroeger op één vervoerder-hit van 3 —
|
||||
Margraten; écht OV-nieuws combineert in de praktijk altijd met een
|
||||
tweede signaal, gevalideerd op 205 productie-artikelen).
|
||||
2. CONTEXT: score = som van unieke groeps-gewichten over titel +
|
||||
samenvatting >= drempel (3) én de titel bevat minstens één signaal.
|
||||
Sterke groepen wegen 2, ambigue 1 (halte/tram/metro/spoorbrug 2).
|
||||
Elke groep telt één keer per artikel — 'bus' + 'buslijn' +
|
||||
'busvervoer' is één bus-signaal; dezelfde groep in titel én
|
||||
samenvatting blijft één signaal. Voorbeelden: bus+halte (1+2),
|
||||
trein+spoorlijn (1+2), metro+'rijdt niet' (2+2), tram+
|
||||
dienstregeling (2+1), sprinter+NS (1+2).
|
||||
|
||||
3. TITEL-ANKER: staat er geen enkel OV-signaal in de titel (geen
|
||||
familie-hit, geen whitelist-term), dan is de score uitsluitend
|
||||
samenvatting-bewijs en mag die geen zwak/ambigue woord bevatten —
|
||||
'station'+'pendelbus'+'bus' in een golfverslag, 'openbaar
|
||||
vervoer'+'zakelijke reizigers' in een economiebericht en 'sterk +
|
||||
zwak uitsluitend in de samenvatting' (beide besluiten 2026-09-12)
|
||||
vallen hieronder. Twee sterke termen in de samenvatting blijven
|
||||
bewust door (delftopzondag 'tram 1, halte Buitenhofdreef',
|
||||
besluit 2026-09-13).
|
||||
3. ANDERS -> wegfilteren. Een artikel mag niet door omdat de summary
|
||||
toevallig een OV-woord bevat ('station'+'pendelbus'+'bus' in een
|
||||
golfverslag, 'openbaar vervoer' als bijzin in een economiebericht);
|
||||
één enkel signaal — zelfs halte/tram/metro/spoorbrug (2) — is nooit
|
||||
zelfstandig voldoende.
|
||||
|
||||
Weert zo '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 cultuurstukje over een 'stationspiano'. 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)
|
||||
# Unieke OV-signalen over beide velden: één term telt één signaal per
|
||||
# artikel ('busstation' in titel én samenvatting, of 60×, blijft één
|
||||
# treffer); _fam_hits levert per term al het hoogste gewicht bij
|
||||
# overlappende familie-treffers.
|
||||
combined = dict(t_hits + s_hits)
|
||||
Woordgrenzen en koppelteken-lookbehinds weren structureel: 'VW-bus'/
|
||||
'schoolbus' (bus-groep), 'stationsstraat'/'stationair'/'stationcar'
|
||||
(station-groep), 'treinstraat'/'treinweg' (trein-groep matchen niet
|
||||
buiten woordgrenzen) — er is geen zwarte lijst nodig. Krant 'De Metro'/
|
||||
café 'De Tram' wegen 2 en combineren alleen; atleet-'sprinter' alleen.
|
||||
"""
|
||||
title_hits = RSSBot._signals(title)
|
||||
combined = dict(title_hits)
|
||||
for group, weight in RSSBot._signals(summary).items():
|
||||
combined[group] = max(combined.get(group, 0), weight)
|
||||
# Stap 1: sterk signaal in de titel is zelfstandig voldoende.
|
||||
for family, name in _compiled_match_strong:
|
||||
if family.search(title):
|
||||
return True
|
||||
# Stap 2+3: context met titel-anker; anders wegfilteren.
|
||||
score = sum(combined.values())
|
||||
strong_title = RSSBot._whitelist_hit(title) or RSSBot._disruption_hit(title)
|
||||
if strong_title:
|
||||
# Stap 1: sterk titel-signaal — +2 bovenop het familie-gewicht van
|
||||
# dezelfde term, waardoor een eenduidige OV-titel alleen voldoet.
|
||||
score += 2
|
||||
if score < MATCH_FILTER_THRESHOLD:
|
||||
return False
|
||||
if not t_hits and not strong_title and 1 in combined.values():
|
||||
# Stap 3: geen enkel titel-signaal én een zwak woord in de score —
|
||||
# samenvatting-bewijs zonder titel-anker is geen OV-bewijs.
|
||||
return False
|
||||
return True
|
||||
return score >= MATCH_FILTER_THRESHOLD and bool(title_hits)
|
||||
|
||||
@staticmethod
|
||||
def _filter_summary() -> str:
|
||||
"""Mensvriendelijke weergave van het vaste gewogen match-filter."""
|
||||
"""Mensvriendelijke weergave van het vaste OV-matchfilter."""
|
||||
return (
|
||||
"**OV-matchfilter (vast in code; max-2-stelsel, precision-first):**\n"
|
||||
"- **Sterk signaal in de TITEL → door**: whitelist-term (NS, ProRail, Nederlandse Spoorwegen, spoorstoring, perron, "
|
||||
"bushalte, conducteur, ov-chipkaart, ovpay, openbaar vervoer, Deutsche Bahn, ICE Berlijn, ...) of een dienststoring-zin "
|
||||
"('sprinters rijden niet', 'metro rijdt niet') — die telt +2 bovenop het familie-gewicht. In de samenvatting alleen "
|
||||
"weegt zo'n term mee als gewone familie.\n"
|
||||
"- **Context (som ≥ 3)**: unieke OV-families over titel + samenvatting. Sterk (2): vervoerders (NS, Arriva, ProRail, Qbuzz, ...), "
|
||||
"spoorinfra (spoorlijn, spoorstoring, perron, ertms, spoorsector), bus-samenstellingen (bushalte, buslijn, buschauffeur, busbaan), "
|
||||
"treinstoring/treinverkeer/treinvertraging, intercity/streekvervoer/veerdienst/buurtbus/belbus, 'lijn 12', ICE-frasen, rangeer..., "
|
||||
"tram, metro, openbaar vervoer, lightrail, halte, pont, spoorbrug, ov-chipkaart/ovpay, ov-staking, conducteur, dienststoring-zin. "
|
||||
"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'/'passagier', 'vervoerder', 'sprinter', 'spoor', 'staking', "
|
||||
"'vervoer', 'busongeluk', 'busvervoer', ... — elke dubbelzinnige term heeft een eigen familie en moet combineren.\n"
|
||||
"- **Eén term = één signaal**: herhaling, beide velden en overlappende familie-treffers wegen niet dubbel. "
|
||||
"Zonder enig titel-signaal (familie-hit, whitelist-term of storingszin in de titel) mag de score geen zwak woord "
|
||||
"bevatten — samenvatting-bewijs is geen OV-bewijs (twee sterke termen in de samenvatting blijven bewust door).\n"
|
||||
"- **Uitgesloten (blacklist)**: 'treinstraat'/'treinweg' (straatnamen rond spoorlijnen); historische stations*-gevallen "
|
||||
"('stationsstraat', 'stationair', 'stationcar', ...) worden al door de woordgrens-familie geweerd"
|
||||
"**OV-matchfilter (vast in code, v1.15, precision-first) — drie regels:**\n"
|
||||
"1. Sterk OV-signaal in de TITEL → door. Sterk = vervoerders (NS, ProRail, Arriva, RET, GVB, Qbuzz, ...), "
|
||||
"spoorjargon (spoorstoring, spoorlijn, wisselstoring, treinstoring, ertms, perron, ...), bus-samenstellingen "
|
||||
"(bushalte, buslijn, busbaan, buschauffeur), tramverkeer/tramlijn, intercity/streekvervoer/veerdienst/buurtbus, "
|
||||
"conducteur, ov-chipkaart/ovpay, openbaar vervoer, 'lijn 12', ICE-frasen, rangeer... en de storingszin "
|
||||
"'vervoerswoord rijdt/rijden (niet) niet'.\n"
|
||||
"2. Context: som van unieke groeps-gewichten over titel + samenvatting ≥ 3 én de titel bevat een signaal. "
|
||||
"Sterke groepen 2 punten; ambigue groepen 1 (halte/tram/metro/spoorbrug 2): 'bus', 'trein', 'station(s)', "
|
||||
"'reiziger'/'passagier', 'vervoer(der)', 'sprinter', 'pendelbus'/'shuttlebus', 'dienstregeling', 'ontsporen', 'spoor'.\n"
|
||||
"3. Anders → wegfilteren. Eén signaal is nooit genoeg; een OV-woord in de samenvatting zonder titel-signaal is "
|
||||
"geen OV-bewijs.\n"
|
||||
"Semantische groepen tellen één keer per artikel ('bus' + 'buslijn' + 'busvervoer' = één bus-signaal); "
|
||||
"'schoolbus'/'VW-bus'/'stationsstraat'/'stationair' matchen structureel niet — geen zwarte lijst."
|
||||
)
|
||||
|
||||
async def _send(
|
||||
|
|
|
|||
Loading…
Reference in a new issue