/// (pattern, replacement) — applied as whole-token, case-insensitive. const ABBREV: &[(&str, &str)] = &[ ("v.a.", "verdacht auf"), ("z.n.", "zustand nach"), ("art.", "arterielle"), ("op", "operation"), ("dd", "differentialdiagnose"), ("re.", "rechts"), ("li.", "links"), ]; /// Fold German umlauts and eszett to their ASCII digraph equivalents so that /// "Hyperlipidämie" and "Hyperlipidaemie" collapse to one canonical token. /// Operates on already-lowercased text (so only the lowercase forms appear). /// Applied to BOTH the indexed corpus text and the query (see lexical.rs), so /// the lexical path stays symmetric under either spelling. NOT applied to the /// embedding path — bge-m3 is multilingual and handles umlauts natively. fn fold_umlauts(s: &str) -> String { let mut out = String::with_capacity(s.len()); for c in s.chars() { match c { 'ä' => out.push_str("ae"), 'ö' => out.push_str("oe"), 'ü' => out.push_str("ue"), 'ß' => out.push_str("ss"), _ => out.push(c), } } out } pub fn normalize(input: &str) -> String { let lower = fold_umlauts(&input.to_lowercase()); let mut out: Vec = Vec::new(); for tok in lower.split_whitespace() { let replaced = ABBREV.iter() .find(|(p, _)| *p == tok) .map(|(_, r)| r.to_string()) .unwrap_or_else(|| tok.to_string()); out.push(replaced); } out.join(" ") }