Page boundaries are not chunk boundaries

Chunking a PDF one page per chunk is the first thing everyone tries, because the extractor hands you pages and pages look like units. They are not. A page break falls wherever the type ran out of room, which is a fact about the layout engine and nothing else.

The result is a set of chunks that begin and end mid-sentence as a matter of design, plus a scattering of footnotes and running headers sitting in chunks whose subject they have nothing to do with.

What a page break is

A page boundary is chosen by the renderer. It has no relationship to the document’s structure — a section may start two thirds of the way down page 4 and end at the top of page 7, and nothing about pages 5 and 6 tells you which section they belong to.

This is worth stating plainly because page-based chunking has an intuitive appeal that survives being explained. A page is a bounded quantity of text of roughly consistent size, which feels like a chunk. It is a bounded quantity of text with a boundary in an arbitrary place, which is the definition of the thing every strategy on this site is trying to avoid.

Two things follow. First, page number is excellent metadata and a bad boundary. Second, everything you want to split on — headings, sections, clauses — has to be recovered from the extracted text, not from the page structure.

This post assumes extraction already happened and produced text in the correct reading order. Getting that right is a separate stage with its own failure modes, and if reading order is wrong, nothing below helps.

The split, shown

A section of a report that straddles a page break, cut per page and then cut structurally with page numbers retained.

=== ONE CHUNK PER PAGE ===
--- chunk (page 12) ---
...decommissioned in the same window. The remaining
dependency is the reporting job described in 4.3, which
reads from the legacy schema directly and must be migrated
before the schema can be
                                    Annual Review 2026 — 12
--- chunk (page 13) ---
Annual Review 2026 — 13
dropped. Migration of the reporting job is scheduled for the
following quarter.

4.4 Cost effects

The consolidation reduces the number of managed instances.
Savings are partly offset by the transitional period during
which both schemas are maintained.¹
--- chunk (page 14) ---
¹ The transitional period was extended by one quarter in the
revised plan.
Annual Review 2026 — 14

Three failures in ten lines. The sentence about dropping the schema is split across two chunks. The running header is inside the text, so a header string appears in the middle of both chunks. And the footnote is in chunk 14, attached to nothing — its marker is on page 13, and a chunk containing only “The transitional period was extended by one quarter” is unattributable to any subject at all.

=== STRUCTURAL, WITH PAGE NUMBERS AS METADATA ===
--- chunk A  {pages: 12–13, section: "4.3 Legacy schema"} ---
[Annual Review 2026 → 4.3 Legacy schema]
...decommissioned in the same window. The remaining
dependency is the reporting job described in 4.3, which
reads from the legacy schema directly and must be migrated
before the schema can be dropped. Migration of the reporting
job is scheduled for the following quarter.

--- chunk B  {pages: 13–14, section: "4.4 Cost effects"} ---
[Annual Review 2026 → 4.4 Cost effects]
The consolidation reduces the number of managed instances.
Savings are partly offset by the transitional period during
which both schemas are maintained.

Footnote 1: The transitional period was extended by one
quarter in the revised plan.

Same source, same extractor. The sentences are whole, the footnote is with the sentence it qualifies, the running header is gone from the body and the page range is still available for citation.

The procedure

1. Concatenate the pages into a single text stream before splitting. Keep a map from character offset to page number as you go. This is the step that makes everything else possible and it is one loop.

2. Remove running headers and footers from the stream. They repeat on every page, which makes them detectable: look at the first and last lines of every page and find the ones that recur with only a digit changing. Do this before splitting, not after, or the repeated strings end up inside chunks.

3. Recover the heading structure from the text. Numbered section headings (4.3, II., Appendix B), lines in a distinct case or weight if your extractor preserves formatting, or lines followed by a blank line and matching a table-of-contents entry. Then split on that structure, which is the general argument applied to a format that hides its structure rather than lacking it.

4. Reattach footnotes to their markers. Collect the footnote text from the bottom of each page, match it to its marker in the body, and place it inline or as a labelled block in whichever chunk contains the marker. Footnotes in legal, academic and financial documents frequently carry the exception that changes the meaning of the sentence, so this is not cosmetic.

5. Carry the page range on every chunk. As a field, so a citation can say “page 13” and a reader can open the document at the right place. Page number is one of the most useful provenance fields available and it is lost the moment you stop tracking offsets.

6. Fall back inside a section, not across the document. If a recovered section is too large, split within it — by paragraph, then by sentence. Never let the fallback reintroduce page boundaries.

What still breaks

Documents with no recoverable headings. A scanned report with no numbering and no reliable formatting signal gives you nothing to split on, and you are back to careful counting over the concatenated stream. That is still strictly better than per-page chunking, because at least the sentences survive.

Multi-column layouts and sidebars, where a pull-quote or a margin note is interleaved into the stream. The text is present and in the wrong place, and no splitting decision fixes text that arrived scrambled.

Tables spanning pages, which come out as two tables, the second with no header. Handle them as tables once you have joined the pages, which is another reason to concatenate first.

Endnotes. Notes collected at the end of the document rather than the foot of the page are far harder to reattach than footnotes, and if you leave them, you get a chunk of numbered fragments belonging to forty different sentences.

Forms and figures. A page that is one diagram and six labels contributes a chunk of nouns. Some pages have no indexable content and it is legitimate to skip them.

How to tell if it worked

Two direct checks, both cheap.

Grep the index for your running-header strings. Any hit inside a chunk body is a header that survived step 2. This should return zero and usually does not the first time.

Look at the first character of every chunk. Sort by it. Chunks starting with a lowercase letter are chunks that began mid-sentence, and after structural splitting that count should be small. It is the fastest signal available that page boundaries are still leaking through, and it is one query rather than an evaluation harness.

Then read a sample, with attention to footnotes specifically: find the chunks containing footnote text and check that each one also contains the sentence it belongs to.