Files
duocthu/docs/pdf-parsing-outlier-catalog.md
T

25 KiB
Raw Blame History

PDF Parsing Outlier Catalog

A generalized checklist of structural risks found while parsing duoc-thu-quoc-gia-viet-nam-2018.pdf (1668 pages). Every item here was confirmed with real evidence (bounding-box inspection, cross-tool comparison, or a whole-document scan) — not assumed. The goal of this document is reuse: if this project (or a future one) needs to parse another structured reference PDF — another national formulary, a different government-published multi-part document, any dense print-layout book — this is the checklist of "things that go wrong that a small page sample won't reveal," and how to actually check for each one cheaply (most checks here run over the whole 1668-page book in under a minute).

For the narrative investigation and drug-formulary-specific numbers, see docs/adr/0003-pdf-parsing-strategy.md. This document is the distilled, reusable checklist form of the same findings, plus items found afterward.


Structural discovery risks (before you even parse content)

1. No bookmarks/TOC

What it looks like: doc.get_toc() (PyMuPDF) returns an empty list. Why it matters: the obvious, easiest structural signal for section boundaries simply doesn't exist — don't design a pipeline that assumes it will. Check: one line, len(doc.get_toc()). Do this first, always, before assuming a bookmark-based approach. Generalizes: yes, directly — always check this before designing around bookmarks, for any PDF.

2. Shallow/unusable tagged-PDF structure tree

What it looks like: the PDF has a /StructTreeRoot (looks promising — "tagged PDF"), but it only covers a handful of generic /H1//P elements for a fraction of the document (here: ~29 elements for 1668 pages). Why it matters: easy to assume "tagged PDF = rich semantic structure available"; in practice many tagging tools produce a minimal compliance-only tree that covers almost nothing. Check: walk the struct tree (doc.xref_object on /StructTreeRoot, recurse into /K) and count real leaf elements vs. total page count. If the ratio is tiny, it's not a usable data source. Generalizes: yes — always verify depth/coverage before trusting a struct tree, don't just check for its existence.


Page layout risks

3. Multi-column body layout

What it looks like: body pages are genuinely two-column (confirmed via bounding boxes: left column x≈44-299, right column x≈308-562, page width ≈595). Front-matter pages that look like a multi-column name grid to the eye turned out, on inspection, to be single wide text blocks with internal whitespace padding between names — not a real structural column split. Why it matters: a naive "read text top-to-bottom regardless of x" pass would interleave left- and right-column content into nonsense. Conversely, assuming every visually grid-like page is column-split leads to wasted effort — verify per page/section, don't generalize from appearance alone. Check: for any suspicious page, dump block bounding boxes (page.get_text("dict")["blocks"]) and look at the actual x0/x1 ranges. A real column split shows two clusters of x-ranges; a padded single-column list shows one wide range per line. Handling: PyMuPDF's default block-level reading order handled the real two-column case correctly here (validated against a known monograph) — the tool most likely to get column order wrong was pdfplumber's general extract_text() (see item 8), not PyMuPDF. Generalizes: yes — this exact check (dump bboxes, look at x-clusters) works on any PDF to determine real column count before writing extraction logic.

4. Full-width content breaking out of the column grid

What it looks like: some pages have a table (or could have a figure) that spans nearly the entire page width (confirmed: a body-surface-area lookup table's blocks span x≈35 to x≈553, i.e. across both normal columns), overriding the page's usual two-column layout. Why it matters: logic written to always split a page into "left column" and "right column" text will misbehave on these pages — the content isn't in either column, it's a single full-width unit. Check: for any block, compare its x-width against the known single-column width; if a block's x-range spans (or nearly spans) both known column ranges, treat it as a full-width unit, not part of a column. Generalizes: yes — any multi-column layout can have occasional full-width breakout elements (tables, figures, pull-quotes); always check for this rather than assuming rigid column adherence everywhere.


Table-specific risks

5. Tables split across a page break lose their header on the continuation page

What it looks like: confirmed directly — "Bảng 4: Xử trí về điều trị ARV theo mức độ phát ban" (a 3-column table) starts on one page with its header row (['Mức độ', 'Biểu hiện', 'Xử trí']) and 3 data rows; its 4th data row ("Mức độ 4...") appears on the next page, extracted by pdfplumber as a separate table object with no header row at all. Why it matters: if a pipeline treats each find_tables()/ extract_tables() result as an independent, self-contained table, the orphaned continuation row is meaningless on its own — you lose the column semantics for that row entirely. Check: for any table-like structure, check whether the page/column immediately preceding it ends with a same-shaped table lacking a natural final row (e.g. an incomplete-looking sequence) — a strong heuristic is "table starts at the very top of a page/column, no header, same column count as the table ending at the bottom of the previous page/column." Handling: never treat page-extracted tables as independent; track continuation explicitly and re-attach the original header to orphaned continuation rows before using them. Generalizes: yes — this is a generic multi-page-table risk in any paginated PDF with tall tables; the detection heuristic (position at page/column top + no header + matching column count to the previous table) applies broadly.

6. Tables can also split across a column boundary on the same page

What it looks like: confirmed — "Bảng 6" (ARV drug toxicity table) starts in the left column near the bottom of a page (header + first data row) and its remaining data rows appear at the top of the right column of the very same page, again with no header repeated. Why it matters: this is easy to miss because there's no literal page break — it's tempting to assume "if it's the same page, it's not split," but a table can still be taller than one column's usable height. Check: same heuristic as item 5, but also check column position, not just page number — a header-less table fragment starting at the top of a column (regardless of page) is a suspect continuation. Generalizes: yes, wherever content flows in columns at all — this risk exists any time column height is shorter than table height.

7. Two-dimensional grid/nomogram tables are not linearly recoverable

What it looks like: confirmed — a body-surface-area lookup table (height across the top, weight down the side, a BSA value at each intersection) extracts as a scrambled sequence of numbers with no recoverable row/column association from plain text alone (e.g. "0,50 0,52 0,54 0,56" followed by "0,55 0,57 0,59 0,61" — these are almost certainly column-wise fragments, not the visual rows). Why it matters: unlike a normal bordered table (rows of related values), a 2D lookup grid's meaning depends entirely on 2D position — a number is meaningless without knowing both its row header (weight) and column header (height). Flattened text extraction destroys exactly the information needed to interpret it. Check: any table where extracted "cells" are bare numbers with no inline label, laid out in a dense grid, is a candidate — cross-check against the source's own stated formula/description (this table is explicitly a lookup version of a stated formula, see item 8). Handling: for RAG purposes, prefer not to chunk this table as literal text at all; either (a) reconstruct it properly using per-number bounding-box position matched against header row/column bboxes (real 2D table reconstruction, non-trivial), or (b) rely on the accompanying formula being available for the LLM to compute from directly, and explicitly flag this table's raw text as unreliable/do-not-cite in metadata. Generalizes: yes — any nomogram, nutrition-fact grid, or nCk-style lookup table in any PDF has this exact problem; detect by the "bare number grid" pattern, don't assume normal table extraction works.


Formula / equation risks

8. Formula rendering is inconsistent — some survive as linear text, some don't

What it looks like: two real formulas found, two different outcomes. The Du Bois body-surface-area formula (simple inline exponents, "S = W0,425 × H0,725 × 71,84") extracted cleanly as readable text. The Cockcroft-Gault creatinine-clearance formula (a stacked fraction — numerator over denominator, visually 2D) extracted as scattered, disordered fragments with no linear reading order. Why it matters: it's tempting to write one rule ("formulas are unreliable, always flag them") or its opposite ("formulas extract fine, no special handling needed") — neither is true here. The determining factor is whether the formula's visual layout is fundamentally 1D (left-to-right, like an inline exponent) or 2D (a fraction, a matrix, stacked terms). Check: no cheap automatic detector was built for this distinction yet — treat any equation/formula-like content as a manual-review candidate, especially anything with a fraction bar, until a real detector exists (e.g. checking for large vertical bbox gaps between adjacent glyphs that should be visually stacked). Generalizes: yes — any technical/medical/scientific PDF with inline math will have this exact split; don't assume all formulas behave the same way in extraction.


Character/glyph-level risks

9. Rare reversed (right-to-left) glyph-order defect

What it looks like: confirmed exactly once across the entire 1668-page book (physical page 1373): one short text run's glyphs are positioned in descending x-order rather than ascending, producing scrambled output (e.g. " = tịx 8 yàgn gnàh uềil gnổt(...") that reverses character-by-character back to the correct Vietnamese sentence ("(4 xịt = 800 microgam) vào buổi chiều..."). Why it matters: this is a genuine, confirmed data-corruption risk, not theoretical — but it's also extremely rare (1 occurrence in 1668 pages), so it must be detected, not assumed to be either absent or common. Check: group text fragments into visual rows by rounded y-coordinate, then check whether x-coordinates are non-decreasing across the row; flag (and optionally auto-correct by re-sorting on x) any row that isn't. This full-book check runs in about 20 seconds. Generalizes: yes, directly — this is a cheap, universal sanity check worth running on any PDF text-extraction pipeline as a standing QA gate, regardless of source document, since it catches a class of PDF-authoring defects (RTL/BiDi overrides, corrupted content streams) that have nothing to do with this book specifically.


Section/heading detection risks

10. Font size is not a reliable heading signal — bold is

What it looks like: confirmed two genuine, equally top-level monograph titles at different font sizes (10.0pt and 9.5pt). An early detector gated on size >= 9.8 and silently dropped ~15% of real monographs as a result. Why it matters: a threshold calibrated from one or two examples will look correct until validated at scale — this is the single clearest "don't generalize from a small sample" lesson from this whole investigation. Check: whole-document validation against an independent ground truth (here, the back-of-book page-numbered index) is what caught this — a sample of 2-3 pages would not have. Generalizes: yes — for any PDF, prefer a binary style signal (bold/not bold, a specific font name) over a numeric threshold (size, weight value) wherever possible, and always validate any numeric threshold against the whole document, not a handful of examples.

11. Multi-line wrapped titles/headings must be merged before matching

What it looks like: confirmed as the dominant cause of missed detections in whole-document validation — long titles (e.g. "CÁC CHẤT ỨC CHẾ HMG-CoA REDUCTASE", "THUỐC TƯƠNG TỰ HORMON GIẢI PHÓNG GONADOTROPIN") wrap across 2+ physical lines; a per-line detector catches only fragments, which then fail to match a name-based ground truth AND can produce false name collisions with an unrelated single-line heading elsewhere in the document (this happened: a wrapped title's second line, "GONADOTROPIN", collided with a genuine, different, single-line "GONADOTROPIN" monograph elsewhere). Check: whole-document recall measurement against ground truth; misses clustered around long/compound names are the signature of this bug. Handling: merge consecutive bold+all-caps lines (with compatible positioning) into one candidate title before matching/keying, rather than treating each line independently. Generalizes: yes — any document with long titles/headings that can wrap will have this exact failure mode; always merge candidate multi-line headings before using them as unique keys.

12a. Class-level monographs cover multiple active ingredients (multiple ATC codes) — this is NOT rare

What it looks like: first noticed via two incidental examples ("GONADOTROPIN", "VITAMIN D VÀ CÁC THUỐC TƯƠNG TỰ"), then actually measured across the whole 680-monograph corpus (not assumed from the 2 examples — this distinction matters, see below). Real, whole-corpus number: 173 of 680 detected monographs (25.4%) have more than one distinct ATC code, ranging up to extreme cases — INSULIN alone lists 20 different ATC codes, BETAMETHASON and DEXAMETHASON 11 each, PREDNISOLON 10, HYDROCORTISON 9. This is a quarter of the entire corpus, not a couple of edge cases — the 2 incidental examples badly understated how common this is, and stating "found 2 examples, pattern confirmed" without the whole-corpus count would have been exactly the kind of unverified claim this project's CLAUDE.md now forbids. Even the 25.4% is a floor, not the true number — see item 12c below: ATC-code text-extraction noise (stray whitespace, O/0 confusion) caused some genuinely multi-ATC monographs (e.g. "TRIAMCINOLON", 5 codes) to be undercounted by a naive regex. The true proportion is measurably higher than 25.4%; re-measure after fixing the regex, don't keep citing 25.4% as final. Why it matters: a data model that assumes "one monograph = one drug = one ATC code" is wrong for roughly a quarter or more of the corpus. Handling: store ATC code (and dosage-form sub-entries) as a list per monograph, not a scalar; when chunking, consider whether a class-level monograph's sections should be tagged with the whole class name, the specific sub-compound, or both, depending on what the retrieval use case needs. Generalizes: yes — any reference work organized primarily by drug class or by generic substance will have entries that don't map 1:1 to a single identifier. More importantly, the methodology generalizes: when you notice a pattern from 1-2 examples, measure its real prevalence across the whole corpus before deciding how much engineering effort it deserves — "found 2 examples" and "25.4% of everything" call for very different levels of investment, and you can't tell which one you're dealing with without the whole-corpus count.

12c. ATC codes (and likely other structured codes) have real text-extraction noise

What it looks like: while investigating why 22/680 (3.2%) monographs appeared to have zero ATC codes, spot-checked 14 of them directly and found two distinct, confirmed causes, both text-extraction noise rather than missing content:

  • Stray internal whitespace splitting one code into two tokens, e.g. "L01X X02" (should be L01XX02), "J04A C01" (should be J04AC01), "N05B A06" (should be N05BA06).
  • Digit/letter confusion: a literal "0" rendered/typeset as the letter "O", e.g. "NO3AX12" (should be N03AX12), "JO1DC07" (should be J01DC07). A relaxed regex tolerating both patterns resolved 9 of the 14 spot-checked cases as real ATC codes hiding behind extraction noise. The remaining ~5 of 14 were genuinely different: the source text explicitly states "Mã ATC: Chưa có." or "Mã ATC: Không có." ("not yet available" / "none") — a real, valid data state, not an error, and not something to paper over as if a code exists. Why it matters: a strict ATC-code regex silently undercounts real ATC data; distinguishing "extraction noise hiding a real code" from "the book says there is no code" requires checking the actual field text, not just whether a regex matched. Handling: normalize ATC-code-shaped text before matching (strip internal whitespace between the letter/digit groups, treat a digit-position "O" as "0") and explicitly check for the "Chưa có"/"Không có" literal strings as a valid "no ATC" state rather than a parse failure. Generalizes: yes — any structured code/identifier extracted from a PDF (product codes, classification codes, reference numbers) can suffer this same whitespace-injection and O/0 confusion; validate structured-looking fields against their expected format and investigate exceptions rather than assuming a strict pattern match is reliable.

12d. A section-title (part-divider) page can be falsely detected as a monograph

What it looks like: confirmed — the very first item in a whole-corpus boundary scan was "CÁC CHUYÊN LUẬN THUỐC" (the literal title of Part 2 of the book, "The Drug Monographs" — a part-divider heading, not a drug) at physical page 98, picked up as a false-positive monograph boundary because it happened to be bold, all-caps, short, and was followed (a few real monograph-boundaries later) by some "Tên chung quốc tế" text from the actual first real monograph. Why it matters: without a whole-corpus scan this would have gone unnoticed indefinitely — it doesn't look wrong from a single-page read of Abacavir, and the discovery methodology this catalog is built on is exhaustive scans, so this is a good example of a defect that only surfaces at full scale. Handling: exclude a small, known set of non-drug part/section-divider strings ("CÁC CHUYÊN LUẬN THUỐC", "CÁC CHUYÊN LUẬN CHUNG", "CÁC PHỤ LỤC", etc. — enumerable from the book's own table of contents) from the monograph-boundary detector, or require the anchor phrase ("Tên chung quốc tế") within a tighter line-distance so an unrelated real monograph several lines away doesn't false-confirm a divider title. Generalizes: yes — any document with part/section-divider title pages styled similarly to its content headings (bold, prominent, short) risks this exact false positive; explicitly exclude known structural/navigational titles from content-boundary detectors.

12b. Genuine spelling/capitalization typos exist in the source text

What it looks like: confirmed real example — the running header on the Vitamin D monograph's continuation pages reads "Vitamin d và các thuốc tương tự" (lowercase "d"), while the real ALL-CAPS heading correctly reads "VITAMIN D VÀ CÁC THUỐC TƯƠNG TỰ". This is a genuine typesetting mistake in the 2018 print, confirmed via font/bbox inspection (same bold 10pt font as the correct heading — not an extraction artifact, the source text itself has the typo). The page's bottom running footer uses yet another variant, the short form "Vitamin D" (correctly capitalized) — meaning the same monograph has three different boilerplate text variants across one page (top header with a typo, the real heading, bottom footer). Why it matters: don't treat running headers/footers as a perfectly clean, typo-free secondary signal (item 13 in this catalog already recommends using them as a cross-check) — they can themselves contain source-level errors. In this specific case, the detection heuristic (strict ALL-CAPS requirement, item 10) happened to still work correctly, because "Vitamin d và các thuốc tương tự" and "Vitamin D" are not fully uppercase and so are correctly rejected as monograph-boundary candidates — but this was not a designed defense against typos specifically, just a side effect of the all-caps requirement. A future/different typo (e.g. an accidentally all-caps running header) would not be caught the same way. Check: no systematic typo-detection was built (out of scope — this is about parsing robustness, not proofreading the source); the practical takeaway is to keep relying on the strict structural signals (bold + all caps + short + anchor phrase) as primary, and treat any single text-based signal (including running headers) as fallible. Generalizes: yes — any real-world print-to-PDF source will have some rate of genuine typos/inconsistencies; parsing logic should be robust to them by relying on multiple independent structural signals (font, position, anchor phrases) rather than trusting any single text match to be error-free.

12e. Monograph length and section coverage vary enormously — measured, not assumed

What it looks like: across all 680 detected monographs, length ranges from 2,331 to 45,623 characters (~20x spread) and the number of known section labels found per monograph ranges from as few as 8 up to 20 (out of a ~19-20 item known vocabulary) — most cluster around 16-19, but the tails are real: "ASPARAGINASE"-adjacent short entries around 2,300-4,300 chars vs. "AMOXICILIN VÀ KALI CLAVULANAT" at 45,623 chars. Why it matters: don't design chunking limits (e.g. a fixed max tokens per monograph, or an assumption that "a monograph roughly fits in N chunks") around a single example — the real distribution has a long tail on both ends. Check: this came from the same whole-corpus survey used for items 12a and 12c — computing length and detected-section-count per monograph is cheap and worth keeping as a standing sanity metric (e.g. flag any monograph outside some percentile range for manual review). Generalizes: yes — any corpus of "similar" documents (monographs, product entries, articles) will have a real length/completeness distribution; measure it before assuming uniformity.

12. The documented taxonomy is not exhaustive — keep it open

What it looks like: the book explicitly documents a 19-field template for every drug monograph (page 38), but real monographs contain at least one undocumented extra field ("Tên thương mại" — brand/trade names) not in that list. Why it matters: treating a documented schema as a closed enum will silently misclassify or drop real content that doesn't fit it. Generalizes: yes — any document that describes its own structure in a preface/README should still be validated against real instances; documented schemas are frequently incomplete in practice.


Noise / boilerplate risks

13. Header/footer boilerplate must be stripped, but can double as a signal

What it looks like: every page carries a page number and a repeating string ("DTQGVN 2"), and body pages additionally carry a running header naming the current monograph/section. Handling: strip the fixed boilerplate before parsing content, but the running monograph-name header is a useful secondary cross-check for "which monograph is this page's body text currently part of" — don't discard it as pure noise. Generalizes: yes — running headers/footers are common in print-derived PDFs and are usually worth extracting as metadata, not just filtering out.

14. Blank/near-empty separator pages at section transitions are expected

What it looks like: exactly 6 near-empty pages (<20 characters) found across the whole 1668-page book, and every single one lands exactly on a major section-transition boundary (before general chapters, before individual monographs, before appendices, near the book's end). Why it matters: a naive pipeline might treat a near-empty page as an extraction failure and error out or flag it, when it's actually an intentional print-layout convention (forcing a new part to start on a fresh page). Check: whole-document scan for pages under some small character threshold; cross-reference their positions against known section boundaries before treating them as errors. Generalizes: yes — this print convention is extremely common in formally typeset books; always expect and gracefully skip near-empty pages rather than treating them as failures.


Not yet investigated (flagged for future work, not silently ignored)

  • Footnote-style superscript reference markers (seen as a, b, c, d in one table) — not yet checked for whether the footnote text stays correctly associated with its marker/row during extraction.
  • Formula detection heuristic (item 8) — no automatic detector exists yet to flag 2D-formula regions before they're trusted as chunk content.
  • 2D grid table reconstruction (item 7) — no implementation yet for recovering row/column-correct values from a nomogram-style table.