Skip to content

Commit a9be45c

Browse files
artizclaude
andcommitted
feat(pdf): merge paragraph fragments split across column/page breaks
docling joins a paragraph whose previous fragment ends mid-sentence (a letter, not sentence punctuation) with a lowercase continuation (`…definition of` + `lists in…` → `…definition of lists in…`); my pipeline emitted two paragraphs. Add a post-assembly pass that merges consecutive top-level paragraph nodes on that rule. A heading/table/figure between fragments ends the paragraph. 2203 277→255, 2305v1 68→56, with smaller gains on 2206/redp5110/normal_4pages; the 4 exact PDFs are unchanged (their paragraphs end with sentence punctuation, so nothing false-merges). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f88efbf commit a9be45c

2 files changed

Lines changed: 35 additions & 0 deletions

File tree

crates/fleischwolf-pdf/src/assemble.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,39 @@ pub fn assemble_page(
587587
}
588588
}
589589

590+
/// Merge paragraph fragments split across a column or page break. docling joins a
591+
/// paragraph whose previous fragment ends mid-sentence (a letter, not sentence
592+
/// punctuation) with a lowercase continuation: `…definition of` + `lists in…` →
593+
/// `…definition of lists in…`. Only consecutive top-level paragraphs merge — a
594+
/// heading/table/figure between them ends the paragraph.
595+
pub(crate) fn merge_continuations(nodes: &mut Vec<Node>) {
596+
let mut i = 0;
597+
while i + 1 < nodes.len() {
598+
let merged = match (&nodes[i], &nodes[i + 1]) {
599+
(Node::Paragraph { text: a }, Node::Paragraph { text: b }) => {
600+
let a_open = a
601+
.trim_end()
602+
.chars()
603+
.next_back()
604+
.is_some_and(|c| c.is_alphabetic());
605+
let b_cont = b
606+
.trim_start()
607+
.chars()
608+
.next()
609+
.is_some_and(|c| c.is_lowercase());
610+
(a_open && b_cont).then(|| format!("{} {}", a.trim_end(), b.trim_start()))
611+
}
612+
_ => None,
613+
};
614+
if let Some(text) = merged {
615+
nodes[i] = Node::Paragraph { text };
616+
nodes.remove(i + 1);
617+
} else {
618+
i += 1;
619+
}
620+
}
621+
}
622+
590623
#[cfg(test)]
591624
mod tests {
592625
use super::clean_text;

crates/fleischwolf-pdf/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ impl Pipeline {
108108
pdfium_backend::for_each_page(bytes, password, |n, _total, mut page| {
109109
self.process_one_page(n, &mut page, &mut doc)
110110
})?;
111+
assemble::merge_continuations(&mut doc.nodes);
111112
Ok(doc)
112113
}
113114

@@ -187,6 +188,7 @@ impl Pipeline {
187188
for (n, page) in pages.iter_mut().enumerate() {
188189
self.process_one_page(n, page, &mut doc)?;
189190
}
191+
assemble::merge_continuations(&mut doc.nodes);
190192
Ok(doc)
191193
}
192194
}

0 commit comments

Comments
 (0)