Document AI

How to Extract PDF Sections Reliably When Headings Are Missing

A robust strategy for PDF section extraction when expected headings are missing, reordered, duplicated, or detected imperfectly.

A common PDF-processing pattern is to remove or extract everything between two section headings.

It works until the next expected heading is missing.

For example, imagine a document with sections A, B, C, and D. You want to extract section B, so the naive implementation searches for B and cuts the text at C. If C is absent in one document, the boundary disappears and the extraction fails.

The robust solution is to stop thinking in terms of one fixed next section and instead detect the next valid heading that actually appears after the start section.

The fragile approach

A simplified version of the fragile logic looks like this:

start = text.find("SECTION B")
end = text.find("SECTION C", start)

if start != -1 and end != -1:
    section_b = text[start:end]

This assumes SECTION C is always present.

Real documents break that assumption. Optional sections may be omitted, templates may change, and OCR may fail to recognize one heading even when it is visible on the page.

Better: search all possible following headings

If the document has a known section order, collect every heading that is allowed to follow the target section and select the earliest one that appears after the start.

def extract_section(text, start_heading, following_headings):
    start = text.find(start_heading)
    if start == -1:
        return None

    candidates = []
    for heading in following_headings:
        position = text.find(heading, start + len(start_heading))
        if position != -1:
            candidates.append(position)

    end = min(candidates) if candidates else len(text)
    return text[start:end]

Now section B can end at C, D, or the end of the document.

If C is missing but D exists, the extraction still works.

Preserve the section order in configuration

For multiple document types, I prefer to keep section order outside the extraction function.

SECTION_ORDER = [
    "SECTION A",
    "SECTION B",
    "SECTION C",
    "SECTION D",
    "APPENDIX",
]

Then the code can derive all valid later boundaries automatically.

def following_sections(section, order):
    index = order.index(section)
    return order[index + 1:]

This is easier to maintain than hard-coding a separate next_section for every extraction rule.

Match headings, not arbitrary text

Using str.find() is enough to explain the idea, but production documents need stricter heading detection.

A heading such as Risk may also appear inside a sentence. If that occurrence is treated as a boundary, the extracted section will be truncated.

Depending on the document format, I use signals such as:

  • beginning of line;
  • surrounding line breaks;
  • numbering patterns;
  • uppercase/title formatting in extracted text;
  • font size or style from layout-aware parsers;
  • page coordinates;
  • a list of normalized heading variants.

For text-only extraction, regular expressions anchored to line boundaries are often a better starting point.

import re

def find_heading(text, heading, start=0):
    pattern = re.compile(
        rf"(?im)^\s*{re.escape(heading)}\s*$"
    )
    match = pattern.search(text, pos=start)
    return match.start() if match else None

Normalize OCR variations

OCR introduces another class of failures. A heading may appear as:

  • SECTION 8 instead of SECTION B;
  • Risk Managernent instead of Risk Management;
  • extra spaces between letters;
  • missing punctuation.

I avoid unrestricted fuzzy matching because it can create dangerous false boundaries. A safer approach is to define known variants for headings that are frequently misread and log when a non-canonical variant is used.

That keeps the behavior explainable.

Handle duplicated headings

Some PDFs repeat section names in headers, tables of contents, or page furniture.

To reduce false matches:

  1. ignore the table-of-contents region when possible;
  2. begin searching after the confirmed start position;
  3. prefer heading-like line structure rather than substring matches;
  4. use page/layout information when available;
  5. validate that the resulting section has a plausible length.

Decide what happens when no later heading exists

The correct fallback is usually domain specific.

Possible choices are:

  • extract until the end of the document;
  • extract until an appendix or signature block;
  • return the section with a warning;
  • reject the document for review.

I prefer making this behavior explicit in configuration rather than hiding it in the parser.

Add tests for missing-section combinations

The most important improvement is testing the negative cases.

If section C is optional, tests should include:

  • B → C → D;
  • B → D with C missing;
  • B as the last section;
  • B missing;
  • duplicated C;
  • OCR variant of C;
  • a mention of C inside normal body text.

That is where document automation becomes reliable: not because the happy path works, but because the parser has defined behavior when the template is incomplete.

For a broader architecture around OCR, extraction, validation, storage, and review, see PDF Document Processing in Python and my PDF Processing Platform.