Chapter 4 of 7 · 13 min read
One click to the highlighted span, with the paragraph around it
On this page
Resolving a citation is the act of turning a stored reference into text on a screen that the reader can read without doing any work of their own. The record from chapter 1 — chunk identifier, character range, stored quote, document version — makes that possible and does not make it happen. Between the record and the reader sits an interface, and an interface that responds to a click by opening a source document at its first page has handed back a document identifier. That is functionally the document-level citation of chapter 1 wearing the span citation's record: the fields are all present and nothing consumes them.
The gap matters because verification is priced in seconds. A reader deciding whether to check a claim is comparing the cost of checking against the cost of being wrong, and a check that takes a minute loses that comparison against almost anything. Deep-linking, the practice of building a URL that carries enough location information to open a source at the cited place rather than at its beginning, is the mechanism most of this chapter is about, and the thing to notice about it early is that the granularity a deep link can express is bounded by what the ingest pipeline recorded. An interface cannot highlight a range nobody stored.
What resolving actually means
The interaction a reviewer needs is a single click that leaves them looking at the quoted text, in place, with enough of what surrounds it to judge whether the quote was fair. That decomposes into four requirements, and each one puts a demand on what the answer pipeline stored.
The viewer has to open at the right location, which needs a page number for paginated formats and a character offset for everything. The quoted text has to be visibly marked, which needs the span's range in the coordinate system the viewer uses, not the chunk-relative one from storage. The reader has to see the text around the quote, which needs paragraph boundaries — the range of the block that contains the span, so the interface can render from paragraph start to paragraph end rather than an arbitrary window that might cut mid-clause. And the reader has to know whether what they are looking at is what the system looked at, which needs the document version that was cited compared against the version being rendered.
The third of those is worth following link by link, because it is the requirement most often dropped and the chain that produces it is short and checkable. A reader is judging whether a quote was fair. Fairness turns on what the quote left out, which means the words immediately outside it. Those words are not in the span by definition, so the interface has to fetch them. Fetching a fixed number of characters either side is a guess about where meaning ends, and a guess that lands mid-clause is worse than useless because it looks complete. What is not a guess is the boundary of the block the span sits in — the paragraph, the table row, the list item — since a block is a unit the document itself defines. Therefore the parser has to emit block boundaries at ingest time. Therefore a parser that flattens cells into a line of text has already destroyed the information, and no amount of work in the interface recovers it.
That turns the citation record from chapter 1 into a locator with a few derived fields, computed once when the answer is written rather than on every click.
{
"citation_id": "a91f3c/c3/0",
"doc_id": "pricing/2025-q3-rate-card.pdf",
"doc_version": "sha256:9f2c41ab…",
"chunk_id": "pricing/2025-q3-rate-card.pdf#tok:1200",
"chunk_span": [412, 477],
"doc_span": [40312, 40377],
"page": 7,
"block_span": [40180, 40598],
"quote": "Growth — included 500,000 calls/mo; overage $0.12 per 1,000 calls",
"viewer_url": "/docs/pricing%2F2025-q3-rate-card.pdf?v=9f2c41ab&p=7&s=40312&e=40377"
}doc_span is the chunk's own document offset added to the chunk-relative range, and it is stored rather than recomputed because the chunk's document offset is a property of the chunking run that produced it. That sentence is checkable against the record itself, and doing the subtraction is what shows why the field is a stored constant rather than a computation. Subtract the two starts: forty thousand three hundred and twelve minus four hundred and twelve is thirty-nine thousand nine hundred. Subtract the two ends: forty thousand three hundred and seventy-seven minus four hundred and seventy-seven is thirty-nine thousand nine hundred as well. One offset carries both ends, which is the arithmetic signature of a translation — the span was moved, not resized. Both ranges are therefore the same width, sixty-five characters, and that width is also the length of the stored quote, exactly as chapter 1's record required.
What the identity licenses is the design decision the field represents. doc_span carries no information that chunk_span and one number do not already carry, so storing it is redundancy bought deliberately: the interface gets a document-coordinate range without having to know anything about chunking, and the offset it was derived from is frozen at answer time. The cost of that choice is equally visible in the arithmetic. If a reindex changes where the chunk starts in the document, the constant changes, and every doc_span in the archive that was derived from the old constant is now wrong while every chunk_span beside it is still right. That is one of the ways the drift in chapter 6 arrives.
block_span is the containing paragraph, cell or list item, produced by the parser at ingest time; a parser that discards block boundaries and emits a flat string forces the interface to guess with a character window, which cuts sentences in half about a third of the time on dense tables. In this record the block runs from forty thousand one hundred and eighty to forty thousand five hundred and ninety-eight, which strictly contains the span at both ends — a few hundred characters before the quote and a few hundred after it. Containment is the property the interface relies on: it can render the whole block and highlight a range inside it without any risk of the highlight falling outside what it drew.
The comparison in the second-to-last step is the cheap part of the design that pays for itself. The interface already has the stored quote and the text the document service just returned; asserting that they match costs a string compare — no fetch, no parse, no model — and turns a silent drift into a visible badge on the exact citation the reader is looking at. What that comparison can say is narrow and worth stating precisely: it reports whether the text at those positions is the text the system cited. It says nothing whatever about whether that text supports the claim, which is a separate question with a separate machinery behind it. When the two strings differ, the honest render is the stored quote labelled as what the system cited, next to the current text labelled as what the document says now.
Where the reader ends up
Opening a new tab is itself a cost, and it is a cost the record has already paid for avoiding: with block_span in hand the interface can render the block under the claim, expanded in place, without a navigation at all. The full document stays one click further away for the cases that need it.
Inline rendering has rules that matter more than they look. The quote is highlighted, and the sentence containing the quote is rendered in full even when the quote is a fragment of it, because a quote clipped at a clause boundary is how a qualifier disappears. In the rate card, the span overage $0.12 per 1,000 calls is a true span of a sentence that continues (annual contracts: $0.09), and a reader who sees only the highlighted fragment has been shown a correct citation and a misleading one at the same time. Render 400 to 600 characters either side of the block, capped at the block boundary, with an ellipsis and an expand control where truncation occurred so that the reader can tell the difference between "this is the end of the paragraph" and "we stopped rendering here".
Tables and lists need their own handling, because a paragraph is the wrong unit inside them. A span landing in a table cell should render the cell together with its column header and its row label, since a highlighted $0.12 with neither is evidence of nothing; the rate card case is exactly this, and rendering the block as the whole table row plus the header row costs a parser that keeps table structure instead of flattening cells into a line of text. List items behave similarly when the stem of the list carries the scope — the following apply to annual contracts only followed by six bullets — so the block for a bullet is the bullet plus its stem.
Those two cases are the same failure as the clipped quote, arriving through layout instead of through punctuation. In each of them the words that fix the meaning of the highlighted text sit outside the highlight: a parenthetical after a clause, a header above a cell, a stem above a bullet. What varies is only which structure holds them, and therefore which boundary the parser must have preserved. Getting the table and list cases right is what removes most of the reviewer escapes that survive an inline render at all, because tables and lists are where dense factual claims live and where a naive character window is least informative.
The version badge has three states rather than two. The cited version and the live version can match, in which case say nothing. They can differ while the text at the span still matches the stored quote, which means the document changed elsewhere and this evidence is intact. Or they can differ with the text at the span no longer matching, which means the evidence has moved or gone, and the interface should say so plainly and offer the archived version. Collapsing the middle case into a general "document has changed" warning is the tempting simplification and it defeats the badge, because most document edits touch none of the cited spans — so the middle state is the common one, and a badge that fires on the common state trains reviewers to dismiss it before reading which state it is reporting.
Choosing how to deep-link
The mechanism available for a deep link depends on what the source is and how much of the viewer you own.
| Mechanism | Granularity | Needs | Fails when |
|---|---|---|---|
PDF fragment #page=7 | Page | Nothing; every PDF viewer honours it | Long pages; no highlight; reader still scans the page |
Browser text fragment #:~:text=, a URL suffix that asks the browser to find a phrase in the page and scroll to it | Phrase | HTML source, a supporting browser | Quote contains punctuation the matcher normalises, text is inside a scrolling widget, or the phrase repeats |
| PDF.js with a text layer, the invisible characters a viewer extracts and positions over the rendered page | Character span | Hosting your own viewer, offsets aligned to the text layer's extraction | Your extraction and PDF.js's differ, so offsets need mapping through the text layer |
| Own document viewer over parsed text | Character span | Storing parsed text per version and rendering it yourself | Layout-heavy documents where the visual form carries meaning |
| Inline block render in the answer view | Character span | block_span from the parser, plus the stored quote | Reader needs the surrounding pages, so keep a full-document link too |
Reading down the granularity column shows the price rising with the precision, which is unsurprising. The gap worth looking at is between the third row and the ones above it, because the kind of cost changes there rather than the amount. Everything above is infrastructure you can buy or borrow: a viewer honours a page fragment, a browser implements text fragments, and if the feature is present it works. From the third row down, the cost is agreement — your text extraction and somebody else's must produce the same character positions for the same document, and there is no version of that you can install. A mapping layer between the two extractions is the only remedy, and it is a component that can be wrong silently, which nothing above it can.
Most systems end up with two of these: an inline block render for the common case and a page-level deep link into the original file for the reader who wants to see the document as published. The intermediate options are worth their cost only when the corpus is homogeneous. If everything is HTML and public, text fragments give you span-accurate links with no infrastructure, and their normalisation quirks are exactly the ones chapter 2's matcher already handles.
Speed, source drift and the limits of one paragraph
Speed of verification is orthogonal to correctness of the citation, and the independence runs in both directions. A wrong citation rendered beautifully is wrong faster. It is also refuted faster, which is a genuine gain rather than a consolation, because refutations that used to cost a minute mostly did not happen at all — the reviewer skipped the check and the wrong claim went out unexamined. Nothing in the interface touches the inputs the checks of chapter 3 consume, so those checks stay exactly as necessary as they were.
Deep-linking also does not survive the source. When the underlying document is a Confluence page that is edited weekly, the offsets stored today address text that will move; the interface can detect that via the stored quote and degrade honestly, and that is the whole of what an interface can do about it. Detection is a read-time operation on one citation, and repair is a write-time operation across an archive, which is why repair belongs to a different component entirely.
And an inline render can mislead by omission in a way a full document cannot. Showing one paragraph is showing the reader what you chose to show. A policy whose meaning depends on a definitions section eight pages earlier is not verifiable from its own paragraph, and no amount of surrounding context solves that, because the missing text is not near the span — it is elsewhere in the document by design. Keeping the route to the full document one click away is therefore the correct affordance for a class of documents where local context is insufficient, rather than a fallback for when the good path fails.
There is a more uncomfortable question waiting, and it is a question about people rather than about parsers. Reviewers who can check a claim in seconds might check every claim, or they might check none, and the two are indistinguishable in a recording of a single afternoon — both look like an interface that is no longer being fought with. Whether the interface changed anything depends on what people do with it in the weeks after it ships, at volumes no usability session reaches, and that has to come off a dashboard rather than out of a room with six reviewers in it.
Citations and attribution — read the current version at https://daviddominguez.dev/learn/en/citations-and-attribution/one-click-to-the-highlighted-span-with-the-paragraph-around-it