"""Typo-tolerant matching for course search (pure, tested). 'phyy' still finds 'Physics'.""" import difflib import re from typing import Optional, Sequence _WORD = re.compile(r"[a-z0-9\u1200-\u137f]+") def words(text: str) -> list[str]: return _WORD.findall(text.lower()) def _common_prefix(a: str, b: str) -> int: n = 0 for x, y in zip(a, b): if x != y: break n += 1 return n def _token_score(token: str, candidate_words: Sequence[str]) -> int: best = 0 for w in candidate_words: if token == w: return 4 if w.startswith(token): best = max(best, 3) elif token in w: best = max(best, 2) else: p = _common_prefix(token, w) if (p >= 3 and p / len(token) >= 0.6) or (len(token) >= 4 and difflib.SequenceMatcher(None, token, w).ratio() >= 0.75): best = max(best, 1) return best def match_score(tokens: Sequence[str], texts: Sequence[str]) -> Optional[int]: """None if ANY token matches nothing; otherwise a higher score = better match.""" candidate_words = [w for t in texts for w in words(t)] total = 0 for token in tokens: score = _token_score(token, candidate_words) if score == 0: return None total += score return total