Skip to content

feat(table): opt-in merging of tokens split by a column boundary - #17

Merged
hallelx2 merged 1 commit into
mainfrom
halleluyaholudele/hal-548-merge-split-tokens
Aug 2, 2026
Merged

feat(table): opt-in merging of tokens split by a column boundary#17
hallelx2 merged 1 commit into
mainfrom
halleluyaholudele/hal-548-merge-split-tokens

Conversation

@hallelx2

@hallelx2 hallelx2 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Adds TableSettings.MergeSplitTokens, off by default.

The problem

The text strategy derives column boundaries by clustering word edges, so a narrow band that happens to align down the page becomes a column — even when it cuts through a value. On 3M's 2018 10-K balance sheet:

| Less: Accumulated depreciation | ( | 16,135) |
|                                | December 3 | 1, |

The document reads (16,135) and December 31,. Nothing is lost, but a consumer treating a cell as one value gets two fragments, and Table.CellsBBox covers only part of the value — which now matters, since those bboxes drive citation highlighting (#15).

Why opt-in, not a fix to the edge derivation

I checked against the reference rather than assuming. pdfplumber 0.11.9 on the same page:

pdfplumber: ['Less: Accumula', 'ted depreciation', '', '(', '16,135)', '(', '16,048']
pdftable:   ['Less: Accumulated depreciation',           '(', '16,135)', '(', '16,048)']

So this is faithful parity, not a defect — and pdfplumber actually splits more, severing the row label too. Changing the default would silently break the byte-compatibility this package promises and would fail the goldens for the wrong reason.

(Incidentally that output also shows pdfplumber losing the closing paren on 16,048, which #14 fixed here — pdftable is now strictly better than the reference on that row.)

Behaviour

MergeSplitTokens=false   ["Less: Accumulated depreciation" "(" "16,135)" "(" "16,048)"]
MergeSplitTokens=true    ["Less: Accumulated depreciation" "(16,135)" "(16,048)"]

MergeSplitTokens=false   ["" "December 3" "1," "December 3" "1,"]
MergeSplitTokens=true    ["" "December 31," "December 31,"]

Note what does not change:

["Total assets" "$" "36,500" "$" "37,987"]

The $ column survives, because there is a real 53pt gutter between the symbol and the number. That is a genuine column on the page, and collapsing it would be a worse failure than the split this fixes.

How the distinction is made

Merging is bounded by TextTolerance — the same threshold word grouping uses — so it only ever rejoins glyphs that Words() would have placed in one word. In the split case the ( ends at 436.90 and the 1 begins at 436.92: 0.02pt apart. A real gutter is orders of magnitude wider.

Cell bboxes are merged alongside the text, so a highlight drawn from a merged cell covers the whole value.

Verification

  • go build, go vet, go test ./... -count=1 -race — all green.
  • All goldens pass unchanged with the flag off, so parity is untouched.
  • Tests pin: the split case at real coordinates, the gutter case (must not merge), multi-fragment runs collapsing in one pass, bbox union, empty neighbours never absorbed, and that the default is off.

Closes HAL-548

Summary by Sourcery

Add an opt-in table setting to merge adjacent cells when a column boundary splits a single token, and wire it into table extraction while preserving existing behavior by default.

New Features:

  • Introduce TableSettings.MergeSplitTokens to optionally merge horizontally adjacent cells whose shared boundary falls within a single token, updating both cell text and bounding boxes for accurate citation highlights.

Documentation:

  • Document the MergeSplitTokens option and its rationale and behavior in the changelog, including its interaction with text-derived column boundaries and citation highlights.

Tests:

  • Add unit tests covering token-split vs real-gutter detection, multi-fragment collapse, bbox union behavior, and that MergeSplitTokens remains disabled by default.

The text strategy derives column boundaries by clustering word edges, so
a narrow band that happens to line up down the page becomes a column even
when it cuts through a value. On a real 10-K balance sheet that yields

  | Less: Accumulated depreciation | ( | 16,135) |
  |                                | December 3 | 1, |

where the document reads "(16,135)" and "December 31,". Nothing is lost,
but a consumer treating a cell as one value gets two fragments, and
Table.CellsBBox then covers only part of the value -- which matters now
that those bboxes drive citation highlighting.

Deliberately opt-in rather than fixed in the edge derivation. Checked
against pdfplumber 0.11.9 on the same page: it produces the same splits
and in fact splits more, breaking the row label into "Less: Accumula" and
"ted depreciation" as well. So this is faithful parity, not a defect, and
changing the default would quietly break the byte-compatibility this
package promises. Callers who want clean values -- feeding a table to an
LLM, say -- turn it on.

Merging is bounded by TextTolerance, the same threshold word grouping
uses, so it only rejoins glyphs Words() would have placed in one word. A
genuine column gutter is far wider than an intra-word gap, so the "$"
column of a financial statement survives untouched; a test pins that,
since collapsing real columns would be a worse failure than the split it
fixes. Cell bboxes are merged alongside the text.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @hallelx2, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@hallelx2, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2dc54f02-cc87-4985-96ac-724605e1fba7

📥 Commits

Reviewing files that changed from the base of the PR and between 7224c2c and 5968500.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • merge_split_test.go
  • page.go
  • table.go

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds an opt-in TableSettings.MergeSplitTokens flag and supporting merge logic to recombine cells whose column boundary splits a token, merges their bboxes, and thoroughly tests the behavior while documenting it in the changelog.

Flow diagram for MergeSplitTokens cell merging behavior

flowchart TD
  A[assembleTableText] --> B{MergeSplitTokens enabled?}
  B -->|false| C[Return Table with original Rows and CellsBBox]
  B -->|true| D[mergeSplitTokens]
  D --> E[Iterate each row]
  E --> F[Iterate each cell in row]
  F --> G{Can merge with previous cell?\ntext non-empty, bboxes non-zero, boundarySplitsToken}
  G -->|yes| H[Concatenate text into previous cell\nUnion bboxes]
  G -->|no| I[Append new cell text and bbox]
  H --> J[Build outRows and outCells]
  I --> J
  J --> K[Return Table with merged Rows and CellsBBox]
Loading

File-Level Changes

Change Details Files
Introduce opt-in MergeSplitTokens setting and merge logic to recombine token-split adjacent cells and their bounding boxes in table extraction.
  • Add MergeSplitTokens bool field and detailed documentation comment to TableSettings
  • Invoke mergeSplitTokens in assembleTableText to optionally post-process rows and CellsBBox
  • Implement mergeSplitTokens row pass that merges text and bbox for horizontally adjacent cells whose shared boundary splits a token within TextTolerance
  • Implement boundarySplitsToken helper that inspects Char positions to distinguish narrow intra-token gaps from real column gutters, and uses glyph midpoints and gap thresholds
page.go
table.go
Add tests that pin merge behavior, safety properties, and default-off semantics for MergeSplitTokens.
  • Add merge_split_test.go with unit tests for boundarySplitsToken on real split vs gutter coordinates
  • Test mergeSplitTokens collapsing multi-fragment runs and merging bboxes
  • Test that genuine separate columns and empty neighbours are not merged
  • Test that DefaultTableSettings and applyDefaults keep MergeSplitTokens disabled by default to preserve pdfplumber parity
merge_split_test.go
Document MergeSplitTokens behavior and rationale extensively in the changelog, including multiple duplicated sections that may need cleanup.
  • Expand CHANGELOG.md with explanation of MergeSplitTokens behavior, pdfplumber parity rationale, and impact on cell bboxes for citation highlighting
  • Append MergeSplitTokens description under multiple version sections, resulting in repeated content that could be deduplicated
  • Note that merging is bounded by TextTolerance so real financial-statement gutters remain separate columns
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@hallelx2
hallelx2 merged commit abe3742 into main Aug 2, 2026
5 checks passed
@hallelx2
hallelx2 deleted the halleluyaholudele/hal-548-merge-split-tokens branch August 2, 2026 09:26
hallelx2 added a commit that referenced this pull request Aug 2, 2026
Follow-up to #17, caught by looking at the output on a real filing
rather than at the unit tests.

Merging was decided row by row. A column boundary is a property of the
TABLE, so that was wrong in a way the tests could not see: on 3M 2018
10-K page 58 the header band contains the split ("December 3" + "1,")
while the data rows below it do not. The header merged, the data rows
did not, and the rows came out with different column counts -- so the
header second date sat above the first column of figures.

  before   |   | December 31, | December 31, |      <- 3 cells
           | Cash | $ | 2,853 | $ | 3,053 |          <- 5 cells

A sheared grid is a worse outcome than the split it set out to fix, and
it is exactly the kind of damage that looks fine in a spot check and
ruins the table for anything consuming it positionally.

Now the decision is made once per boundary across every row, then
applied uniformly, so the table stays rectangular:

  after    |   | December 31, | December 31, |
           | Cash and cash equivalents | $2,853 | $3,053 |

The trade is that one row containing a split collapses that boundary for
the whole table. That is the right direction: rectangularity matters more
to a consumer than per-cell purity, and the merged result reads correctly
anyway.

TestMergeSplitTokensKeepsTableRectangular pins it with a table whose
first row splits and whose second does not, asserting equal column counts
rather than only the merged text -- the shearing is what the previous
tests missed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant