Match a flat list of OCR names against guild members and print a report.
(label: str, all_names: list[str])
| 306 | |
| 307 | |
| 308 | def report(label: str, all_names: list[str]) -> None: |
| 309 | """Match a flat list of OCR names against guild members and print a report.""" |
| 310 | # Deduplicate keeping last occurrence (mirrors "best activeness" logic) |
| 311 | seen: dict[str, int] = {} |
| 312 | for name in all_names: |
| 313 | seen[name] = seen.get(name, 0) + 1 |
| 314 | |
| 315 | matched: dict[str, tuple[str, int]] = {} |
| 316 | unmatched: list[tuple[str, int, str, float]] = [] |
| 317 | |
| 318 | for ocr_name, count in seen.items(): |
| 319 | m, r = best_match(ocr_name) |
| 320 | if r >= THRESHOLD: |
| 321 | if m not in matched or count > matched[m][1]: |
| 322 | matched[m] = (ocr_name, count) |
| 323 | else: |
| 324 | unmatched.append((ocr_name, count, m, r)) |
| 325 | |
| 326 | missing = [m for m in GUILD_MEMBERS if m not in matched] |
| 327 | |
| 328 | print(f"\n{'=' * 60}") |
| 329 | print(f" {label}") |
| 330 | print(f" Matched {len(matched)}/{len(GUILD_MEMBERS)} guild members") |
| 331 | print("=" * 60) |
| 332 | |
| 333 | if missing: |
| 334 | print(f"\n Missing ({len(missing)}):") |
| 335 | for m in missing: |
| 336 | print(f" {m!r}") |
| 337 | |
| 338 | if unmatched: |
| 339 | print(f"\n Unmatched OCR reads (ratio < {THRESHOLD}):") |
| 340 | for ocr, cnt, m, r in sorted(unmatched, key=lambda x: -x[1]): |
| 341 | print(f" {ocr!r:32s} x{cnt} best={m!r} ({r:.2f})") |
| 342 | |
| 343 | print("\n All matched:") |
| 344 | for gm, (ocr, cnt) in sorted(matched.items(), key=lambda x: x[0].lower()): |
| 345 | flag = " *" if ocr != gm else "" |
| 346 | print(f" {gm!r:30s} <- {ocr!r:30s} x{cnt}{flag}") |
| 347 | |
| 348 | |
| 349 | # ── Activeness parsing ──────────────────────────────────────────────────────── |
nothing calls this directly
no test coverage detected