Snap vs Anchor in LiteParse's Text Projection System: What's the Difference?

Snap is an internal, temporary grid classification used during line grouping, while Anchor is the final, persistent alignment attribute stored on each ProjectedTextItem that drives output formatting.

In the run-llama/liteparse crate, the projection pipeline transforms raw PDF text into structured lines. Understanding the difference between Snap and Anchor is critical for debugging layout reconstruction or building custom output formatters, because these two enums represent sequential but distinct stages of the text projection algorithm.

What Is the Difference Between Snap and Anchor?

At the highest level, Snap is a volatile, internal heuristic, whereas Anchor is a stable, public-facing property.

  • Snap (used internally as SnapKind) lives only inside crates/liteparse/src/projection.rs while the algorithm determines line breaks and column edges. It classifies raw items as belonging to a left-snapped, right-snapped, or centered grid line.
  • Anchor is assigned after snapping completes and is stored permanently on every ProjectedTextItem. It describes the item's logical alignment within its final line and is consumed by output modules such as crates/liteparse/src/output/text.rs.

This separation prevents transient snapping errors from reaching the renderer.

Snap: A Transient Grid Classification

Snap is defined in crates/liteparse/src/types.rs at lines 78–82. It is an enum with three variants—Left, Right, and Center—that tells the projector how raw items line up on a snapped y-grid.

During projection, the algorithm groups TextItems whose y-coordinates fall into the same grid cell. It then evaluates the distribution of x-coordinates within that group to decide whether the line is a left-snapped margin, a right-snapped column edge, or a centered block. This logic appears around lines 1735–1740 of crates/liteparse/src/projection.rs, where the code builds three AnchorMaps and selects a SnapKind for each block.

Because snapping is iterative, these values can be overwritten as the grid refines. An incorrect Snap classification can cause text to merge across columns, so this step is critical for multi-column layout detection.

Anchor: The Final Logical Alignment

Anchor is defined immediately after Snap in crates/liteparse/src/types.rs at lines 86–90. While it mirrors the same three variants, its role is fundamentally different: it records the final logical alignment of an item inside its line after the projection algorithm has finished.

Once the snap heuristic stabilizes, the promotion to Anchor happens inside the loop that constructs ProjectedTextItems, specifically at lines 2581–2589 of projection.rs. At this point, every item receives an immutable anchor value that will travel through the rest of the pipeline. The anchor field is stored directly on ProjectedTextItem alongside the temporary snap field, and it is serialized into the final ParsedPage for downstream consumption.

How the Projection Pipeline Bridges Snap and Anchor

The separation between these two enums enforces a clean boundary between unstable heuristics and stable output contracts.

  1. SnapKind resolution – The projector quantizes coordinates, compares them against AnchorMaps, and assigns a tentative SnapKind.
  2. Anchor assignment – After the snap decision is finalized, the algorithm copies that value into the anchor field of each new ProjectedTextItem.
  3. Output formatting – Code in crates/liteparse/src/output/text.rs reads the immutable anchor value to insert the correct spacing, ignoring the temporary snap.

This three-stage flow ensures that even if the snapping logic shifts during grid refinement, the rendered output relies on a single, deterministic alignment value.

Code Examples: From SnapKind to Anchor

Constructing a ProjectedTextItem

The following excerpt, adapted from crates/liteparse/src/projection.rs, shows how the transient snap is promoted to a durable anchor when an item is instantiated:

// Inside projection.rs – after the snap has been resolved
let snap_kind = SnapKind::Left;               // result of the snapping step
let mut projected = ProjectedTextItem {
    item: raw_text_item.clone(),
    snap: snap_kind,                          // temporary snap kind
    anchor: match snap_kind {                 // final alignment
        SnapKind::Left   => Anchor::Left,
        SnapKind::Right  => Anchor::Right,
        SnapKind::Center => Anchor::Center,
    },
    is_dup: false,
    rendered: false,
    num_spaces: 0,
    force_unsnapped: false,
    is_margin_line_number: false,
    rotated: false,
    d: 0.0,
    orig_x: raw_text_item.x,
    orig_y: raw_text_item.y,
    orig_width: raw_text_item.width,
    orig_height: raw_text_item.height,
    orig_rotation: raw_text_item.rotation,
};

Note that snap retains the internal grid classification, while anchor locks in the public alignment that formatters will use.

Rendering Output with Anchor

Downstream modules such as output/text.rs consume the stable anchor field to assemble the final text string. The simplified renderer below demonstrates how alignment affects spacing:

fn render_line(items: &[ProjectedTextItem]) -> String {
    let mut line = String::new();
    for itm in items {
        match itm.anchor {
            Anchor::Left   => line.push_str(&format!("{} ", itm.item.text)),
            Anchor::Right  => line = format!("{} {}", itm.item.text, line),
            Anchor::Center => line.push_str(&format!("{} ", itm.item.text)),
        }
    }
    line.trim_end().to_string()
}

By this stage, the temporary snap value is irrelevant. Only the immutable anchor determines whether an item is placed at the beginning, end, or center of its line.

Impact on Output Formatting

The anchor field directly controls spacing behavior in the text output stage. According to the implementation in crates/liteparse/src/output/text.rs, items carrying Anchor::Center are rendered without leading spaces, whereas items with Anchor::Left or Anchor::Right receive left- or right-justified spacing. This distinction allows LiteParse to faithfully reconstruct centered headers, flush-right page numbers, and standard left-aligned body text from complex PDF layouts.

Summary

  • Snap is a temporary, internal classification used only inside crates/liteparse/src/projection.rs while the algorithm snaps raw items onto a discrete y-grid and resolves line breaks.
  • Anchor is the persistent alignment attribute stored on every ProjectedTextItem, consumed by output formatters to produce correctly spaced final text.
  • Both enums are defined in crates/liteparse/src/types.rs—Snap at lines 78–82 and Anchor at lines 86–90—and share the same variants, but they are kept distinct to isolate heuristic instability from rendering logic.
  • The promotion from SnapKind to Anchor occurs at lines 2581–2589 of projection.rs, after which the value becomes immutable for the remainder of the pipeline.

Frequently Asked Questions

Can Snap values change after they are first assigned?

Yes. Snap is deliberately transient and may be overwritten as the projection algorithm refines the snapped grid and re-evaluates AnchorMaps. It exists solely to help the projector decide how items group into lines. Only after the heuristic stabilizes does the value graduate into the immutable Anchor field.

Is the Anchor enum part of LiteParse's public API?

No. Both enums are marked #[doc(hidden)] in crates/liteparse/src/types.rs, so they are omitted from the official public API documentation. However, the anchor field remains present in serialized ProjectedTextItem structures, making it available for debugging and custom formatter integrations.

Why does LiteParse maintain two separate enums for the same alignment concepts?

The separation enforces architectural clarity. Snap represents a volatile, grid-relative heuristic that can shift during computation. Anchor represents a fixed, logical property of the final output. Decoupling them prevents transient snapping errors from propagating to the text renderer and makes the projection logic easier to test and maintain.

Where can I find the code that turns a Snap decision into an Anchor value?

The assignment occurs in crates/liteparse/src/projection.rs between lines 2581 and 2589. This loop runs after the snapping heuristic has decided a block's SnapKind, copying that result into the anchor field of every newly created ProjectedTextItem before the item proceeds to the output stage.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →