SevyDevy

Guide · AI and GenAI

PDF Parsing for RAG: How to Handle Tables, Scans and OCR

30 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • PDF parsing for RAG means turning each PDF into clean, ordered text and tables before search sees it. RAG, short for retrieval-augmented generation, finds passages and hands them to a model to answer from.
  • A digital PDF stores a text layer, the actual characters you can copy. A scanned PDF is only a picture, so it needs OCR (optical character recognition): software that reads letters from an image and adds its own errors.
  • Plain extraction often mixes two columns line by line, repeats headers and footers, and flattens tables into loose numbers. In our test, pdfplumber 0.11.10, an open-source Python library, read straight across a two-column page.
  • Store each table row as a line that repeats its column names. Save the page number, the method (text layer or OCR) and the OCR confidence, its own score of how sure it is, with every piece.
  • Beginners can stop after the section on checking quality. Engineers then get tested results from pdfplumber, pypdf and the Tesseract 5.3.4 OCR engine. They include a 5-degree tilt that scrambled OCR, a table OCR could read only cell by cell, and fonts that return codes like (cid:1) instead of letters.

What goes wrong when you feed PDFs straight into a RAG system?

Feeding PDFs straight into a RAG system goes wrong because the text that comes out of a PDF is often not the text a person reads. Columns get mixed, tables lose their column names and scanned pages come out empty. PDF parsing for RAG is the step that fixes this before search.

Picture a common setup. A district land-records office keeps about 40,000 PDFs: sale deeds, land record extracts and mutation orders, which record a change of owner. Many are scans of old paper with round registration stamps over the text. The setup is made up, but the problem is real.

The office builds an assistant with RAG. The program first searches the documents for passages that match a question. Then it hands those passages to a large language model, the AI program that reads text and writes the answer.

A citizen asks a simple question: what is the area of plot 214 under khata 87? A khata is a land account number that can hold several plots. The assistant replies 1.86 acres.

A records clerk opens the PDF and reads the table by hand. Plot 214 is 0.42 acres. The figure 1.86 is the total for the whole khata, printed in the last row.

Here is the text the parser handed to search, from our test copy of that page. It came from pdfplumber 0.11.10, an open-source Python library for reading PDFs.

text
Land record extract, khata 87
Khata Plot Area (acres) Land type
214 0.42 Homestead
87 215 0.61 Paddy
216 0.83 Paddy
Total 1.86

Look at where 87 landed: on the line for plot 215, not plot 214. The khata cell is merged across three rows, so its text sits in the middle row. Nothing ties 0.42 to Area or 214 to Plot any more.

Search found the right page, but the page no longer made sense, so the model picked the wrong number. The real bug was in parsing, the step before everything else.

Which words do you need before learning PDF parsing for RAG?

You need six words: parsing, text layer, scanned PDF, OCR, layout and reading order. Each one gets a plain meaning and an example from the records office.

Parsing. Parsing means turning a file into structured pieces your program can use, like paragraphs, headings and table rows. Example: turning a 12-page sale deed into 40 clean paragraphs and one table, each tagged with its page number.

Text layer. A text layer is the list of characters stored inside a PDF, with where each one sits on the page. When you can select and copy words in a PDF viewer, you are using its text layer.

Scanned PDF. A scanned PDF is a picture of paper saved as a PDF. On its own it has no text layer, so copying from it gives you nothing. Example: a 1994 sale deed that a clerk fed through a scanner.

OCR. OCR, short for optical character recognition, is software that looks at a picture of text and works out the letters. Example: reading the words on that 1994 deed so they can be searched.

Layout. Layout is how things are arranged on a page: columns, headings, tables, pictures, headers and footers. Example: a mutation order printed in two columns, with a stamp in one corner.

Reading order. Reading order is the order in which a person reads the parts of a page. On a two-column page, you read all of column one before column two, not straight across.

What is PDF parsing for RAG, and what should it produce?

PDF parsing for RAG is the step that turns each PDF page into clean records that search can use. Search and the model only ever see these records, never the PDF itself.

A useful page record holds five things. Each one has a job later in the pipeline:

  • The text, in reading order, with tables kept as rows.
  • The page number, so an answer can point to the exact page.
  • The method: text layer or OCR, because OCR text needs more checking.
  • The OCR confidence: Tesseract, a widely used open-source OCR engine, scores each word from 0 to 100.
  • The position of each region on the page, so a reviewer can jump straight to it.

The last three fields cost almost nothing to save. Without them, you cannot tell a good page from a bad one later.

What is the difference between a digital PDF and a scanned PDF?

A digital PDF was made by a computer program and usually has a text layer, while a scanned PDF is only a picture of paper. You can pull text out of the first directly, but the second needs OCR.

The simple test is to extract text from each page. If a page gives back almost nothing, it is probably a scan. Check page by page, not file by file, because one PDF can mix both kinds.

The records office has many mixed files. A sale deed might have 10 typed pages made on a computer, then 2 scanned pages of signed annexures, the extra pages attached to the deed. So the parser decides for every page on its own.

There is a trap. Some scanners run their own OCR and hide its text under the picture, so the page looks digital. That hidden text is only as good as the scanner's OCR. Other files use fonts with no map from shapes to letters, so extraction returns codes like (cid:1). A page can pass the length test and still be garbage.

The diagram shows how each page is routed. Look at the question box in the middle: it chooses between the fast path and the slower OCR path.

How the records office routes each PDF page

The choice is made per page, and a page with unreadable text goes to OCR even when it has a text layer.

Diagram connections:

  • Sale deed PDF arrives to Split into pages
  • Split into pages to Readable text layer?
  • Readable text layer? to Use the text layer (Yes)
  • Readable text layer? to Render 300 DPI and OCR (No)
  • Use the text layer to Save page records
  • Render 300 DPI and OCR to Save page records
  • Save page records to Hand off to chunking
  • Sale deed PDF arrives: Typed and scanned pages mixed
  • Split into pages: Each page is checked on its own
  • Readable text layer?: 50+ characters and no (cid:) codes
  • Use the text layer: Fast, with no OCR errors
  • Render 300 DPI and OCR: Slower, and every word gets a confidence
  • Save page records: Text, page, method, confidence
  • Hand off to chunking: The next step, covered in its own guide

Here is that routing as Python you can run. It uses pdfplumber for the text layer and pytesseract, a small Python wrapper around the open-source Tesseract OCR engine. Install them with pip install pdfplumber==0.11.10 pytesseract==0.3.13, plus Tesseract itself (apt install tesseract-ocr on Ubuntu).

python
import unicodedata
import pdfplumber
import pytesseract

MIN_CHARS = 50  # made-up cut-off: fewer characters means "probably a scan"


def looks_readable(text):
    if "(cid:" in text:  # a font with no Unicode map: shapes, not letters
        return False
    bad = text.count("\ufffd") + sum(
        1 for ch in text
        if unicodedata.category(ch).startswith("C") and ch not in "\n\t")
    return bad / max(len(text), 1) < 0.05


def parse_page(page, number):
    text = page.extract_text() or ""
    if len(text.strip()) >= MIN_CHARS and looks_readable(text):
        return {"page": number, "text": text, "method": "text layer",
                "confidence": None}
    image = page.to_image(resolution=300).original.convert("L")
    data = pytesseract.image_to_data(image, lang="eng",
                                     output_type=pytesseract.Output.DICT)
    words = [(w, float(c)) for w, c in zip(data["text"], data["conf"])
             if w.strip() and float(c) >= 0]  # -1 marks non-word rows
    mean = sum(c for _, c in words) / len(words) if words else 0.0
    return {"page": number, "text": " ".join(w for w, _ in words),
            "method": "ocr", "confidence": round(mean, 1)}


def parse_pdf(path):
    with pdfplumber.open(path) as pdf:
        return [parse_page(p, n) for n, p in enumerate(pdf.pages, start=1)]


for rec in parse_pdf("sale_deed.pdf"):
    print(rec["page"], rec["method"], rec["confidence"], rec["text"][:30])

The function looks_readable rejects text with (cid: markers or more than 5 percent control characters, which are invisible codes that are not letters. MIN_CHARS is a made-up cut-off: a typed page holds hundreds of characters, so 50 is a safe low bar.

For OCR, the page is drawn as a picture at 300 DPI, which means dots per inch, a measure of sharpness. Then image_to_data returns every word with its confidence. Rows that are not words carry a confidence of -1, so the code drops them before taking the average.

One honest gap: text-layer pages keep pdfplumber's default reading order. The next section shows why two-column pages need one more fix.

We ran it on a three-page test deed: one typed page, one scanned page, and one page whose font has no letter map. This is the output, with each text cut to 30 characters:

text
1 text layer None Plot 214 under khata 87 measur
2 ocr 95.1 Land Records Office, Page 1 of
3 ocr 82.2 Fee © 1,250 paid. TdT 87, plot

What just happened? Page 1 had a good text layer, so no OCR ran. Page 2 had no text at all and went to OCR, scoring 95.1. Page 3 had 66 characters, enough to pass the length test, but its (cid: codes sent it to OCR.

Page 3 also shows a second trap. Its text holds a rupee sign and a Hindi word, but the code asked for English only. So the rupee sign came back as a copyright sign and the Hindi word as TdT, and the confidence dropped to 82.2.

Tesseract uses only the language models you name, and it falls back to English when you name none. Each model must also be installed. For Odia and English pages you would pass lang="ori+eng". In our test, an English page read with only the Odia model came out as junk, while ori+eng read it perfectly.

Why does text extracted from a PDF come out jumbled?

Text comes out jumbled because a PDF mainly stores where to draw each character, not which sentence or paragraph it belongs to. An extractor must rebuild the reading order, and both common ways of doing that can fail.

The first way follows the order in which the file draws its text. The second way ignores that order and sorts words by their position on the page, top to bottom and left to right.

Picture reading a newspaper by going straight across the page, from the left column into the right one, line by line. Every word is correct, yet the sentences make no sense. That is what position sorting does to a two-column page.

Where the picture breaks: you see the white gap between columns at a glance. An extractor sees only numbers for each character's position, so a narrow gap or a heading across both columns can fool it.

Reading order on one two-column test page (tested 22 October 2026)
MethodFile drawn column by columnFile drawn line by line
pypdf 6.21.2, extract_textCorrect orderLines from both columns mixed
pdfplumber 0.11.10, extract_textLines from both columns mixedLines from both columns mixed
pdfplumber with use_text_flow=TrueCorrect orderLines from both columns mixed
pdfplumber, each column cropped firstCorrect orderCorrect order
Tesseract 5.3.4 OCR on a 300 DPI picture, narrow gapLines from both columns mixedSame picture, same result

What just happened? Only cropping each column read both files correctly. pypdf, another open-source Python PDF library, followed the file's drawing order in our test. That works only when the file was written in reading order, which you cannot see by looking at the page.

pdfplumber's default groups words into lines by their vertical position, then reads each line left to right, so it reads straight across. Its source code shows this ordering, and its use_text_flow option switches to the file's order instead. Cropping works on both files, but you must know where the columns are.

Some PDFs carry tags, a hidden structure that marks headings, paragraphs, tables and the correct reading order. W3C accessibility guidance, from the group that writes web standards, says the tag order mainly decides reading order for screen readers. Tags exist only when a file was saved as tagged PDF, and a plain scan has no text to tag at all.

These are the problems the records office found when it read its parsed text:

  • Columns: mutation orders printed in two columns came out with lines from both columns mixed together.
  • Headers and footers: a line like Land Records Office, Page 3 of 12 was repeated on every page, and thousands of times in the search index.
  • Page breaks: page numbers and running titles landed in the middle of sentences that crossed from one page to the next.
  • Broken words: in our test, both libraries kept regis- and tered as two pieces, so a search for registered could miss them.
  • Stamps: the words inside a registration seal got mixed into the paragraph printed underneath.
  • Tables: rows and columns were flattened into loose numbers, exactly like the plot 214 bug.

For headers and footers, compare the first and last line of every page. Replace digits with a placeholder first, because Page 1 of 3 and Page 2 of 3 differ only in their numbers. On our three-page test file, this found both repeated lines.

Move those lines out of the main text, but keep them as metadata, which means extra facts stored next to the text. A footer may hold a document number someone will ask about later.

How does OCR work, and what mistakes does it make?

OCR works by cleaning a page image, finding the lines and words on it, and then recognising each character. Its mistakes come from blurry or tilted images, look-alike characters, the wrong language setting and ink printed over the text.

The diagram follows one scanned page of a 1994 sale deed. Notice the decision near the end: pages with low confidence go to a person instead of straight into storage.

OCR on one scanned sale deed page

Cleaning comes before reading, and unsure pages get a human check before they reach search.

Diagram connections:

  • Scanned deed page to Clean the image
  • Clean the image to Find lines and words
  • Find lines and words to Recognise characters
  • Recognise characters to Page average above 85?
  • Page average above 85? to Store the page text (Yes)
  • Page average above 85? to Records clerk reviews (No)
  • Records clerk reviews to Store the page text (Corrected text)
  • Scanned deed page: Tilted picture of paper, stamp over text
  • Clean the image: Straighten, black and white, trim borders
  • Find lines and words: Draws a box around each text line
  • Recognise characters: Each word gets a confidence, 0 to 100
  • Page average above 85?: A made-up cut-off
  • Store the page text: Marked as OCR, with its score
  • Records clerk reviews: Fixes the text or asks for a re-scan

The cleaning step matters more than most beginners expect. Tesseract's documentation says it works best on images of at least 300 DPI. It also says line finding gets much worse on a skewed page, and dark scanner borders can be read as extra characters.

Our tests agree, with one twist. On a clean typed test page, 50 DPI produced junk and 72 DPI made small errors. From 100 to 400 DPI the text came out perfect, so treat 300 DPI as a safe default for real scans with small print and smudges.

Tilt hurt far more. The same page rotated by 1 or 3 degrees still read perfectly. At 5 degrees Tesseract 5.3.4 broke lines into pieces and scrambled their order. At 8 degrees, or 5 degrees the other way, it returned nothing. So straighten pages before OCR.

Here are the errors that hurt land records the most:

  • Look-alike characters: at 72 DPI our test read plot 215 as 218 and acres as acras. A plot number with a letter O in place of a zero never matches a search.
  • Lost decimal points: if a faint dot vanishes, 0.42 acres becomes 042, a very different piece of land.
  • Stamps and signatures: ink pressed over typed words turns them into nonsense characters.
  • Wrong language setting: an English page read with only the Odia model came out as junk, as shown above.
  • Tables in scans: Tesseract's documentation says it has trouble with tables unless the layout is found first, and our test agreed.

Tesseract gives each word a confidence from 0 to 100, and the page score is whatever average you compute. On our test page, 72 DPI gave a mean of 77.7 with errors, while 100 DPI and above gave 93 to 96 with none. A made-up cut-off of 85 would have flagged the bad page.

A low score does not prove a word is wrong, and a high score does not prove it is right. It tells you where to look first.

How do you extract tables from PDFs for RAG?

Extract tables by keeping their structure: which value sits in which row, under which column. Then store each row as a short line of text that repeats the column names, so every row makes sense on its own.

Go back to the plot 214 bug. The flattened text lost the link between each number and its column. A row line keeps it: Khata: 87, Plot: 214, Area (acres): 0.42. Now a search for plot 214 finds a line that answers the question by itself.

Finding the structure is the hard part. A table finder first locates the table, then works out its rows, columns and merged cells. By default, pdfplumber's table finder follows the ruled lines drawn on the page, and a text setting handles tables without lines.

W3C guidance for tagged PDFs names the same parts: a table, its rows, header cells and data cells. Our extract_table call on the land record page returned this list of rows:

text
[['Khata', 'Plot', 'Area (acres)', 'Land type'],
 ['87', '214', '0.42', 'Homestead'],
 [None, '215', '0.61', 'Paddy'],
 [None, '216', '0.83', 'Paddy'],
 ['', 'Total', '1.86', '']]

None means pdfplumber found no cell at that spot, because the merged khata cell above covers it. An empty string means a real cell with nothing in it. Telling the two apart is how you know which value to copy down.

Land records show three table problems again and again. Long tables run across pages, with the column names printed only on the first page. Some cells are merged, like one khata number spanning three plot rows. And the total row looks like any other row unless you mark it.

Each problem has a plain fix. Carry the column names forward to the next page. Copy a merged cell's value into every row it covers. Keep the word Total in the row line, so nobody mistakes 1.86 for the area of one plot.

This function turns those rows into lines. It copies merged values down, skips empty cells, and sends any row with the wrong number of cells for review instead of guessing. extract_table returns None when it finds no table, so check for that before you call it.

python
import pdfplumber


def table_to_lines(rows, doc_name, page):
    headers, lines, above = rows[0], [], {}
    for row in rows[1:]:
        if len(row) != len(headers):  # broken row: do not guess
            lines.append({"text": " | ".join(c or "" for c in row),
                          "page": page, "needs_review": True})
            continue
        cells = []
        for i, cell in enumerate(row):
            if cell is None:  # no cell here: covered by a merged cell above
                cell = above.get(i, "")
            above[i] = cell
            cells.append(cell)
        pairs = [f"{h}: {c}" for h, c in zip(headers, cells) if c]
        lines.append({"text": f"{doc_name}. " + ", ".join(pairs),
                      "page": page, "needs_review": False})
    return lines


with pdfplumber.open("land_record.pdf") as pdf:
    rows = pdf.pages[0].extract_table()
for line in table_to_lines(rows, "Land record, khata 87", page=1):
    print(line["text"])

This is the output on our test page:

text
Land record, khata 87. Khata: 87, Plot: 214, Area (acres): 0.42, Land type: Homestead
Land record, khata 87. Khata: 87, Plot: 215, Area (acres): 0.61, Land type: Paddy
Land record, khata 87. Khata: 87, Plot: 216, Area (acres): 0.83, Land type: Paddy
Land record, khata 87. Plot: Total, Area (acres): 1.86

What just happened? Plots 215 and 216 now carry khata 87, copied from the merged cell. Every line starts with the document name, so the row still says where it came from once it leaves the page. The total row reads Plot: Total, so it cannot pass for one plot.

Row lines are not the only choice. Many teams also store the whole table as a small text grid, which helps with questions about the full table, like listing every plot under khata 87. The table below compares the common options.

Four ways to store one land record table (values made up)
FormatWhat the stored text looks likeGood forWeak spot
Flattened text214 0.42 Homestead 87 215 0.61 PaddyNothing, this is what you want to avoidNumbers lose their column names
One line per rowKhata: 87, Plot: 214, Area (acres): 0.42Questions about one plotQuestions about the whole table need many lines
Text grid with barsHeader line, then one row per line, cells split by a barQuestions about the whole tableA long table may be too big for one search piece
Structured recordJSON (a simple text format for data) with one key per columnExact filters and sums in codeNeeds its own lookup, not only text search

To check a parsed table, compare it with the true one. One research paper on table recognition treats each table as a tree: the table holds rows, and each row holds cells. It scores how many edits turn one tree into the other, where 1 means identical.

You do not need the maths to use the idea. A table is right only when each value sits in the right cell.

How do you check PDF parsing quality before indexing?

Check parsing quality in three ways: compare sample pages with the originals, run automatic checks on every page, and ask questions with known answers. Do this before the text reaches the search index, because a bad parse is cheap to fix early and hard to spot later.

Start with a measure for text. Edit distance counts the smallest number of single-character changes (add, remove or replace) that turn the parsed text into the true text. Divide it by the length of the true text to get the character error rate, which you can compare across pages.

python
def edit_distance(a, b):
    prev = list(range(len(b) + 1))
    for i, ca in enumerate(a, 1):
        cur = [i]
        for j, cb in enumerate(b, 1):
            cur.append(min(prev[j] + 1, cur[j - 1] + 1,
                           prev[j - 1] + (ca != cb)))
        prev = cur
    return prev[-1]


def error_rate(parsed, truth):
    return edit_distance(parsed, truth) / len(truth)


print(edit_distance("0.4Z acres", "0.42 acres"))
print(error_rate("0.4Z acres", "0.42 acres"))
print(round(error_rate("Plot 2l4 is O.42 acres", "Plot 214 is 0.42 acres"), 3))

It prints 1, then 0.1, then 0.091. Reading 0.4Z instead of 0.42 is one edit in ten characters, an error rate of 0.1. The last line has two look-alike errors, a lowercase l and a letter O, in 22 characters.

You only need a small set of your own pages with the correct text typed out. Then the same numbers tell you whether a parser change helped.

  1. Pick a sample

    Choose 50 pages, a made-up number. Pick most at random, then add extra scans, two-column orders and tables, since those fail most often.

  2. Compare by hand

    A records clerk types the true text for those pages. Measure the error rate per page and check every table cell by cell.

  3. Run automatic checks on every page

    Flag empty pages, (cid: codes, low OCR confidence, lines repeated on most pages, and table rows with the wrong number of cells.

  4. Check numbers with rules

    Plot areas must add up to the khata total, and every plot number must contain only digits.

  5. Ask known questions

    Keep a list of 30 questions with known answers, like the area of plot 214. Check that the right passage shows up in the parsed text and in search results.

  6. Re-run after every change

    Run the same checks whenever you change the parser, the OCR settings or the cleanup code.

Keep the original PDFs and the parse output apart, and label the output with the parser version. When the parser improves, you can re-parse everything and compare old and new results page by page.

What did our tests on pdfplumber, pypdf and Tesseract show?

Our tests showed that the defaults of all three tools can mislead you on ordinary pages. Each result below came from small made-up test PDFs, so treat them as warnings to test your own files, not as general accuracy figures.

OCR and extraction tests on made-up land-record pages (Tesseract 5.3.4, run 22 October 2026)
TestResultWhat to do
Resolution, clean typed page50 DPI junk, 72 DPI small errors, 100 to 400 DPI perfectRender scans at 300 DPI
Tilt, same page at 300 DPIFine at 1 and 3 degrees; lines broken and scrambled at 5; empty at 8Straighten pages before OCR
Two columns, narrow gap, 300 DPIRead straight across both columns; a wider gap was read column by columnSplit columns before OCR
Ruled table, whole pageOnly the title came out, plus stray lettersOCR one cell at a time
English page, Odia model onlyJunk; ori+eng read it perfectlyName every script on the page
Speed, one pageText layer about 9 ms (short page) or 0.12 s (dense page); OCR about 2 s or 20 sOCR only pages that need it

Two results are undocumented as of 22 October 2026, so pin your version. At 72 DPI, Tesseract happened to read the two columns in order, while at 300 DPI it read across them. With the gap widened from about 2 cm to 3.3 cm, it found the columns, so rely on neither result. And empty table cells produced text, as the next section shows.

The speed numbers come from a two-core test machine and time the first read of a freshly opened page. They exclude the time to draw each page at 300 DPI. OCR was roughly 150 to 250 times slower, which is why you should run it in a background job at upload time.

How do you OCR a scanned table without losing its cells?

OCR a scanned table by finding its grid first, then reading each cell on its own as a single line of text. Reading the whole page at once failed completely in our test.

We drew the land record table at 300 DPI and ran Tesseract on the full picture in four page modes (3, 4, 6 and 11). Every run returned the title line plus a few stray letters, like fc, or the number 87, never the plot rows. That matches the documentation's warning about tables.

Then we cut out each cell, using the cell boxes from pdfplumber, and read it with mode 7, which treats the image as one text line. Every filled cell came back right, including Area (acres) and 0.42.

The two empty cells in the total row came back as junk, BS and ee. A simple ink check fixed that: we skipped any cell where under 0.5 percent of pixels were dark. Both empty cells had none, and the junk was gone.

Our cell boxes came from the digital page, which a real scan does not have. On a real scan you must find the grid lines in the image itself. Tesseract's documentation points to custom layout analysis for this.

Why does a PDF with a text layer still return gibberish?

A PDF can return gibberish because its text layer stores font codes, not letters. A ToUnicode map inside the PDF tells extractors which letter each code stands for. If the map is missing or incomplete, the extractor can only guess.

We tested this by deleting the map from a page that read: Fee, a rupee sign, 1,250 paid, khata in Hindi, 87, plot 214. The English letters and digits still came out right, because the library that made our test file kept their usual codes.

The rupee sign and the Hindi word did not. pdfplumber returned (cid:1) for the rupee sign and four more codes for the word. pypdf returned invisible control characters instead.

The pdfminer.six documentation, the library pdfplumber reads PDFs with, explains the (cid:x) output: some PDFs have incomplete Unicode mappings. So count (cid: markers and control characters on every page, as looks_readable does, and send failing pages to OCR.

What is layout-aware parsing, and when do you need it?

Layout-aware parsing first finds the regions on a page, such as titles, paragraphs, tables, pictures and footers. Then it handles each region in its own way. You need it when your documents have columns, tables, stamps or pictures, which covers most real business PDFs.

A plain extractor reads a page as one stream of characters. A layout detector, usually a machine learning model trained on many labelled page images, draws a box around each region and names it. A reading-order step then sorts those boxes the way a person would read them.

A public research dataset shows what these region names look like. This human-labelled dataset of 80,863 pages uses 11 labels: caption, footnote, formula, list item, page footer, page header, picture, section header, table, text and title. Its pages come from financial reports, manuals, scientific articles, laws, patents and government tenders.

The diagram shows layout-aware parsing on one two-column mutation order. Watch the headers and footers: they skip the reading-order step and leave the main text, but they are not thrown away.

Layout-aware parsing of a mutation order

Each kind of region gets its own handling, and only the main regions go through reading order.

Diagram connections:

  • Mutation order page to Layout detector
  • Layout detector to Reading order
  • Reading order to Headings and paragraphs
  • Reading order to Tables
  • Reading order to Pictures and stamps
  • Layout detector to Headers and footers (Not main text)
  • Headings and paragraphs to Clean page record
  • Tables to Clean page record
  • Pictures and stamps to Clean page record
  • Headers and footers to Clean page record (Saved as metadata)
  • Mutation order page: Two columns, a table and a stamp
  • Layout detector: Boxes and names every region
  • Reading order: Column one first, then column two
  • Headings and paragraphs: Kept with their section title
  • Tables: Rebuilt as rows with column names
  • Pictures and stamps: Caption or short description kept
  • Headers and footers: Kept out of the main text
  • Clean page record: Text, rows, metadata, page and box

A layout detector is a model, so it makes mistakes too. Run your hardest pages, such as two-column orders and long tables, through the quality checks above before you trust any parser.

What should you do with images, stamps and captions?

Keep each caption attached to its picture, and turn important pictures into short text that search can find. A text search index cannot look at a picture, so a site plan with no words beside it is invisible to the assistant.

The records office handles three kinds of pictures. Site plans keep their caption plus a short description, written by a person or by a model that reads images and marked as generated. Stamps are stored as facts, such as registration stamp present on page 2.

Signatures are noted as present and never sent through OCR. The stamp fact turns out to be useful: when someone asks whether a deed was registered, the assistant can point to page 2, where a person can check the stamp.

How do you explain PDF parsing for RAG in a system design interview?

Explain it as a pipeline with checks: route pages by type, parse the layout, keep tables structured, store metadata, and measure quality before indexing. Interviewers want to see that you know parsing errors flow into every later step.

  1. Ask about the documents

    Find out how many files there are, how many are scanned, which languages and scripts they use and how often new ones arrive.

  2. Route each page

    Use the text layer when it is readable, and OCR on a straightened 300 DPI page image when it is not.

  3. Parse the layout

    Find regions, fix the reading order, and move headers and footers into metadata.

  4. Keep tables whole

    Turn rows into lines with column names, copy merged cells down, mark total rows, and send broken rows for review.

  5. Store metadata

    Save the file name, page number, region box, parse method and confidence with every piece.

  6. Measure and review

    Describe the sample checks, the automatic flags, the known questions and a person reviewing low-confidence pages.

Questions people ask

What is the best way to parse PDFs for RAG?
There is no single best tool, because the right approach depends on your documents. Route each page by type, use layout-aware parsing for columns and tables, and run OCR only on pages without a readable text layer. Then test on your own hardest pages, since a parser that does well on one kind of file can fail on another.
Do I need OCR for every PDF?
No. Use OCR only on pages that lack a readable text layer, such as scans and photos of paper. OCR is far slower, and it can add errors the text layer never had, such as look-alike characters. In our test it was over 150 times slower. Check page by page, because one file can mix digital and scanned pages.
What resolution should scanned pages have for OCR?
Use about 300 DPI. Tesseract's documentation says it works best on images of at least 300 DPI. In our test on a clean typed page, 72 DPI already caused small errors, and a 5-degree tilt did far more damage than that. Straighten pages and trim dark borders as well.
How do I stop tables from breaking in RAG?
Detect the table as a table, then store each row as a line that repeats its column names. Carry column names across page breaks, copy merged cells into every row they cover, and mark total rows. Keep the page number with each row so answers can point to it.
Should I convert PDFs to Markdown for RAG?
It can help. Markdown is a plain text format with simple marks for headings and tables, so it keeps more structure than bare text. The conversion is only as good as the layout detection behind it, though. Check converted tables and two-column pages against the original before you trust the output.
Can a model that reads page images replace PDF parsing?
Sometimes. A model that reads page images directly can handle layouts that simple tools get wrong. It usually costs more per page than reading a text layer, and its mistakes read as fluent text, so it needs the same checks as any parser. A sensible split is to send it only the pages that simpler parsing gets wrong.
How do I remove headers and footers from PDF text?
Find lines that repeat at the top or bottom of most pages, after replacing digits with a placeholder so page numbers do not hide the repeat. Move them out of the main text. Keep them as metadata, since a footer may hold a document number someone will ask about later.
How can I tell whether a PDF page is scanned?
Extract its text layer and count the characters. A page with almost none, or with (cid: codes, should go to OCR. In our test, a scanned page gave 0 characters and pdfplumber listed 1 image on it. Check every page, not just the first, because files often mix both kinds.

Sources and further reading

Practise this with Lala

Practise