-
max_calls_for — Return the per-search issue-list call budget, or None for unlimited.
-
threshold_for — Return the per-budget filter threshold in
[0, 1]. -
max_results_for — Return the per-budget cap on volumes/series to expand into issue queries.
-
should_keep_volume_name — Decide whether a discovered volume is worth a per-volume issue lookup.
comicbox.formats.base.online.series_filter¶
source module comicbox.formats.base.online.series_filter
Pre-call series-name fuzzy filter for online sources.
Each source’s volume/series discovery step returns up to N hits for a free-text series query. CV’s full-text search will return “Lois Lane: A Celebration” and “Lois Lane and Friends” alongside the canonical “Lois Lane (2019)” — and the matcher’s s_series signal would score the non-matches near zero anyway. Issuing the per-volume list_issues API call for those non-matches is wasted budget under CV’s 200/hr cap.
This module provides the pre-call filter both sources consult before deciding to spend an API call on a discovered volume. The same normalize_series from signals.py is reused so a candidate that passes the filter scores predictably under s_series downstream.
Why fuzz.ratio and not fuzz.WRatio (which s_series uses): WRatio applies a partial-match boost that lifts any substring overlap to 90+, including “Lois Lane” vs “Adventures of Lois Lane and Friends” (both score 90). That’s fine for scoring — the matcher’s downstream signals can still rank candidates — but useless for filtering, where we need to drop substring-overlapping-but-different volumes before issuing an API call for them. Plain ratio (Levenshtein) gives:
Lois Lane vs Lois Lane (1986) → 78
Lois Lane vs Watchmen (1986) → variant year tail OK
Lois Lane vs Watchmen Annotated → 62
Lois Lane vs Adventures of Lois Lane ... → 41
A natural threshold around 0.70 separates “same series, possibly with a year/volume suffix” (keep) from “different series that happens to share words” (drop). The pre-filter is intentionally STRICTER than the matcher’s s_series; the matcher gets to be permissive because it sees the full candidate. The pre-filter has to decide before any API call goes out.
Driven by the Effort resolved per-source. The thresholds below are calibrated against the fixture set, not placeholders; each carries the measurement that chose it.
Functions
source max_calls_for(budget: Effort) → int | None
Return the per-search issue-list call budget, or None for unlimited.
None means THOROUGH (or an unrecognized budget): no per-search cap, matching that mode’s “spend freely” contract.
source threshold_for(budget: Effort) → float
Return the per-budget filter threshold in [0, 1].
source max_results_for(budget: Effort, *, default: int) → int
Return the per-budget cap on volumes/series to expand into issue queries.
default is the source’s class-level production cap (20 today). Returns the per-budget override when set; otherwise the default.
source should_keep_volume_name(profile_series: str | None, volume_name: str | None, threshold: float, *, alt_names: Iterable[str] = ()) → bool
Decide whether a discovered volume is worth a per-volume issue lookup.
Returns True (keep, spend the API call) when either:
threshold <= 0— the filter is configured as no-op for the resolved API budget. This isBALANCEDuntil Phase B says otherwise.profile_seriesis missing — we have no comparison anchor; never drop on missing data (would risk false-negatives for tagged-without-series fixtures).volume_nameis missing — likewise, can’t compare; keep.- The rapidfuzz
ratiosimilarity between the two normalized names is>= threshold * 100. Same normalization ass_series(so naming conventions agree), different primitive (so we filter substring noise the matcher would still score high). See module docstring for rationale. - Any of the volume’s
alt_namesclears that same bar. Aliases are purely additive — they can rescue a volume filed under a localized or variant title (“Shingeki no Kyojin” for a comic tagged “Attack on Titan”) but never drop one the primary name would have kept.
Returns False (skip, save the call) otherwise. The caller is expected to log a debug-level reason when this happens so calibration runs can audit pre-filter false-negatives.