"""Unscharfer Namensabgleich – aus dem Kassenzettel-Scan, jetzt auch für Rezepte. Lag bis dahin in ``routers/products.py``. Hierher gezogen, weil der Rezeptabgleich dieselbe Frage stellt: Welcher Artikel ist mit „Mehl" oder „MÜHLEN SCHNITZ" gemeint? Zwei Kopien derselben Heuristik würden mit der Zeit auseinanderlaufen. """ from __future__ import annotations import difflib import re _UMLAUTE = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss"}) def normalisieren(text: str) -> str: """Klein, Umlaute aufgelöst, nur Buchstaben/Ziffern – Preise/Sonderzeichen weg.""" text = (text or "").lower().translate(_UMLAUTE) return re.sub(r"[^a-z0-9]+", " ", text).strip() def match_score(zeile: str, name: str) -> int: """Ähnlichkeit 0–100 zwischen gesuchtem Text und einem Namen. Kombiniert die Gesamt-Ähnlichkeit mit einem Token-/Präfix-Abgleich, damit abgekürzte Kassennamen ("MÜHLEN SCHNITZ") auf den vollen Namen passen – und damit eine Rezeptzutat „Mehl" den „Weissmehl Typ 550" findet. """ a, b = normalisieren(zeile), normalisieren(name) if not a or not b: return 0 gesamt = difflib.SequenceMatcher(None, a, b).ratio() a_tok, b_tok = a.split(), b.split() bester = [] for t in a_tok: m = 0.0 for u in b_tok: if u.startswith(t) or t.startswith(u): m = max(m, min(len(t), len(u)) / max(len(t), len(u))) else: m = max(m, difflib.SequenceMatcher(None, t, u).ratio()) bester.append(m) token = sum(bester) / len(bester) if bester else 0.0 return round(100 * max(gesamt, token))