adds a metadata-only recovery pass that scans matched figure assets for
internal PDF line labels ("Figure N.", "Fig. N:") when the figure lacks
a figure_number.
new components:
- extract_pdf_lines_normalized() in ocr_pdf_spans.py — reads PDF rawdict
lines page-by-page, normalizes coordinates to OCR space
- _recover_missing_figure_numbers_from_assets() — inventory pass between
synthetic fallback and dedup
- _needs_asset_internal_figure_number_recovery() — gate supporting both
synthetic_figure and figure_unknown entries
- _looks_like_internal_figure_label() — regex-based label detection
- _asset_edge_band_score() — geometric rejection for center-positioned
lines or lines covering >15% asset area
- page_pdf_lines_by_page parameter added to build_figure_inventory()
verification: U746UJ7G figure_unknown_000 -> figure_002 with recovered
label "Plot of Criteria Time". 428 regression tests pass (6 new).
- preserve PyMuPDF dir/wmode in span_metadata and bump span backfill version
- promote rotated vision_footnote figure descriptions into normal legend prematch
- normalize rotated caption/asset regions before score_figure_match
- carry rotation_correction_deg into matched figures and cropped renders
- use LANCZOS resampling on rotated figure crops to avoid aliasing
- keep synthetic fallback as last resort, reusing the same rotation metadata
- Add _apply_bbox_only_synthetic_vector_fallback helper
- Score by page/x-overlap/vertical-gap with hard gates at 300px and x_ratio<0.25
- Tie-breaking: skip when top-two scores differ by <0.15
- Duplicate figure number prevention against existing matched_figures
- Inserted after _infer_missing_main_figure_numbers in pipeline
- No PNG rendering — bbox-only, render layer shows placeholder
- Minimum score threshold 0.65 to prevent false positives
- Add _FIGURE_DESCRIPTION_OPENING_PATTERN for 'This figure...' / 'Figure N' etc.
- Insert rescue before generic footnote fallback
- Route to figure_caption (near media) or figure_caption_candidate (far)
test_previous_page_locator_bridge_cross_page_full_legend:
p15 full legend (in rejected_legends) + p16 locator + 3 assets.
Verifies: settlement_type, legend_page, page, text (full not locator),
bridge_block_ids, all 3 asset_block_ids, cluster_bbox present,
full legend removed from unmatched_legends, assets from unmatched_assets.
test_previous_page_locator_bridge_does_not_swallow_other_group:
p8 locator + Fig.8 3-asset group + unrelated 'other' asset.
Verifies: only the 3-asset group consumed; 'other' stays unmatched.
Add _infer_missing_main_figure_numbers() that fills figure_number=None
for matched main-sequence figures when a single leading gap (Figure 1)
can be inferred with high confidence.
- _FRONTMATTER_VISUAL_VETO + _has_frontmatter_visual_veto()
- _FIGURE_MARKER_PATTERN regex + _extract_figure_marker()
- _coerce_int_figure_number(), _resolve_legend_bbox()
- _infer_missing_main_figure_numbers() wired into build_figure_inventory()
- 9 test cases covering acceptance, veto, isolation, skip reasons
- Fix golden vault test key drift (aliases, ocr_time, etc.)
When two figures share the same figure_id (e.g. supplementary
"Figure S.1" and main "Figure 1" both mapped to "figure_001"),
the second occurrence is renamed with an "s" prefix:
figure_001 -> figure_s001, figure_ss001, etc.
This avoids file overwrite in ocr_objects.py and ensures both
body and supplementary figures appear at their correct positions
in the rendered fulltext.
Changes:
- _resolve_figure_id_collisions() in ocr_figures.py
- figure_id passthrough in ocr_figure_reader.py (reader + normalize)
- _reader_figure_embed_target prefers explicit figure_id
- 11 unit tests covering single/triple/multi collisions, mixed buckets,
empty IDs, realistic body+supplementary layout
- _strip_caption_number_prefix + _normalized_caption_body helpers
- When same figure number but different caption body (and not bundle-source),
retain both as distinct legends instead of deduping
- Fixes 2UIPV93M page 49 appendix figures silently removed by dedup
- Bundle-source dedup (DWQQK2YB pattern) unaffected
- same_number_distinct_legends audit surface added to inventory
- 217 tests passed, 0 failures
- Changed _reserve_cross_page_objects to return set[tuple[int, str]]
instead of set[str] so reused block_id across pages does not
overwrite reserved legends (VFS8CBW2 page 33 vs 38 both used block_id=1)
- Updated _settle_cross_page_reserved_objects to key legends_by_id
by (page, block_id)
- Fix: reserved legends no longer filtered by caption-band assist on
competing-caption pages, so deferred hypotheses still emit
- Fix: reservation test assertion updated for tuple format
- Fix: competing-caption veto test updated for band-scoped parent
acceptance
Part A: namespace separation - add _extract_figure_namespace and _format_figure_id helpers, change dedup key from int to (namespace,int) tuple so Figure 1 and Supplementary Figure 1 do not collide, update figure_id generation across all paths, add figure_namespace field to matched entries.
Part B: page assets gate - suppress page_assets candidate groups on pages with competing captions (multiple figure_caption blocks) so one group cannot swallow assets meant for multiple figures.
Tests: test_main_and_supplementary_figures_do_not_dedup_into_one_number, test_page_assets_does_not_strict_match_when_page_has_competing_captions
Add figure legend completeness check ensuring every numbered formal
legend lands in an explicit outcome bucket (matched/held/ambiguous/
unresolved_cluster/unmatched). No legend may disappear without inventory
explanation.
Changes:
- Add compute_figure_legend_completeness() to ocr_figures.py
- Integrate completeness into build_figure_inventory() return value
- Add figure_legend_completeness fields to health report (ocr_health.py)
- Gaps degrade overall health status and add degraded reasons
- Add 9 new tests for completeness edge cases
- Inline figure mention: _looks_like_inline_figure_mention() detects body prose
such as 'Figure X shows/illustrates/demonstrates' and 'as shown in Figure X'
to prevent noise in figure caption pipeline. Excludes Frontiers FIGURE N | format.
- Layout audit: added safety net so pages with layout confidence < 0.7 never
produce ERROR-level anomalies (only INFO). Check 2 (insert_body_overlap) also
gated by layout confidence.
- DWQQK2YB matched figure count dropped 6->3 as inline mentions correctly excluded;
M36WA39N Frontiers captions unaffected