fix: lower German-teaser threshold for short job-room descriptions

job-room's search API returns a short keyword-anchored teaser (always
< 500 chars), not the full posting. The >=6 DE/FR-word threshold was
calibrated against full page text and is unreachable on a couple of
sentences, so a genuinely German teaser (Consult & Pepper's "Senior
Fullstack Software Engineer") could clear it on word count alone despite
jobs.ch itself showing "Deutsch (Fliessend)" on the full posting. Use a
lower threshold for short text; the full-page >=6 threshold stays as-is
for longer descriptions, where it was tuned to avoid false positives.
This commit is contained in:
2026-09-10 10:39:54 +02:00
parent 2e21d55807
commit 7cd1f9254d
+19 -3
View File
@@ -43,9 +43,18 @@ function applyFilters(title: string): boolean {
const MS_STACK_RE = /\.net\b|\bc#|sharepoint|dynamics 365|power platform|\bsccm\b|intune|successfactors|\bsap\b/i;
// DE/FR indicator words, counted against a description we already have in hand.
// Mirrors the ≥ 6 threshold isEnglishContent uses on fetched pages.
// Mirrors the ≥ 6 threshold isEnglishContent uses on fetched pages — but see
// the adaptive threshold below for why short snippets need a lower bar.
// du/dein*/dich/dir/einem/einer/eines/der/dem added 2026-09-08: job-room's
// search API "Senior Fullstack Software Engineer" (Consult & Pepper) teaser —
// "Du arbeitest in einem agilen Umfeld nach Scrum mit folgendem Tech-Stack:
// ... TypeScript, Angular, HTML, CSS, PostgreSQL, ..." — is entirely German
// but has zero umlauts and only one hit ("mit") on the original list, so it
// sailed through as English despite jobs.ch itself showing "Deutsch
// (Fliessend)" on the full posting. These are unambiguous German/French
// function words with no common English collision.
const DE_FR_WORDS_RE =
/\b(und|oder|wir|Sie|Ihre|Ihrer|Kenntnisse|Erfahrung|Aufgaben|Anforderungen|Stelle|Unternehmen|Mitarbeiter|Bereich|nous|vous|votre|pour|avec|des|les)\b/g;
/\b(und|oder|wir|Sie|Ihre|Ihrer|Kenntnisse|Erfahrung|Aufgaben|Anforderungen|Stelle|Unternehmen|Mitarbeiter|Bereich|nous|vous|votre|pour|avec|des|les|du|dein\w*|dich|dir|einem|einer|eines|der|dem)\b/g;
// English-language AND not demanding German above B1 — mirrors isViableContent/
// isViableUrl in fetch.ts, but against a description we already have in hand
@@ -61,7 +70,14 @@ function isViableDescription(description?: string): { ok: boolean; reason?: stri
if (!description) return { ok: true }; // nothing to judge — let the URL check in index.ts decide
const text = description.replace(/<[^>]+>/g, " ").replace(/&[a-z]+;/g, " ");
if (hasNonEnglishUmlaut(text)) return { ok: false, reason: "non-English (umlaut)" };
if ((text.match(DE_FR_WORDS_RE) ?? []).length >= 6) return { ok: false, reason: "non-English" };
// job-room's search API returns a short keyword-anchored teaser (measured:
// always < 500 chars), not the full posting — ≥6 is calibrated against full
// page text and is unreachable on a couple of sentences, so a genuinely
// German teaser can clear it on word count alone. Lower the bar for short
// text; leave the full-page threshold (≥6) alone for longer descriptions,
// where it was tuned to avoid false positives on English text.
const threshold = text.length < 500 ? 3 : 6;
if ((text.match(DE_FR_WORDS_RE) ?? []).length >= threshold) return { ok: false, reason: "non-English" };
if (requiresGerman(text)) return { ok: false, reason: "requires German > B1" };
return { ok: true };
}