# Metadata Reference

Every metadata key the engine emits, per format and mode, with types and optionality — verified against live output.

`metadata` is a map whose keys depend on **both the format and the mode**.
[Output Schema](/docs/output-schema) explains the object; this page enumerates
the keys.

Every table below was verified against live output at default parameters
(`window_size` 3, `overlap` 1, `sentences_per_chunk` 3,
`paragraphs_per_page` 15) over the project's fixture corpus.

  A key listed here is emitted by that format in that mode — it is not a
  guarantee for any other combination, and a key that *is* emitted can still be
  `null`. Use `chunk["metadata"].get(key)` (Python), a presence check on
  `chunk.metadata[key]` (JS), or `chunk.metadata.get(key)` on the
  `serde_json::Value` (Rust). Key names are identical in all three SDKs — only
  the chunk's own `content_type` is camelCased (`contentType`) in JavaScript.

## Two chunker families

Which table applies to your file depends on which chunker handles it.

- **The shared pipeline** — PDF, Markdown, HTML, TXT, RTF, JSON/JSONL/NDJSON,
  EML/MBOX/MSG, ODT/ODP, EPUB, IPYNB. One implementation, so the per-mode key
  set is *the same* for all of them, plus a small set of format-specific
  additions.
- **Native chunkers** — DOCX, DOC, PPTX, PPT, and the spreadsheet / delimited
  families. Each has its own key vocabulary.

## The shared pipeline

### Core keys by mode

Emitted by every pipeline format in that mode — but **not on every chunk**. The
derived modes pass some blocks through untouched, and a pass-through chunk
carries the vocabulary of what it *is* rather than of the mode that produced it.
The exceptions are worth knowing before you index on a key —
[they are enumerated and verified below](#keys-that-are-absent-not-null):

| Mode | Key | Type |
| --- | --- | --- |
| `default` / `structural` | `section_heading` | `str \| null` |
| | `document_metadata` | object — [see below](#document_metadata-by-format) |
| `section` | `section_heading` / `section_level` | `str \| null` / `int` |
| | `heading_path` | `list[str]` |
| | `paragraph_count` / `char_count` | `int` / `int` |
| | `block_types` | `list[str]` — `heading`, `paragraph`, `list`, `table`, `code_block` |
| | `chunk_index` | `int`, 0-based |
| | `split_part` / `split_total` | `int \| null` — set only when one section was too large and was split |
| `semantic` | `section_heading` / `section_level` / `heading_path` | as above |
| | `paragraph_count` / `chunk_index` | `int` |
| | `keyword_density` | `float`, 0–1 |
| | `merge_reasons` | `list[str]` — every signal that fired |
| | `primary_merge_reason` | `str` — the winning one ([values](#enumerated-values)) |
| `sliding_window` | `window_index` / `window_size` / `overlap` | `int` |
| | `paragraph_range` | `[int, int]`, inclusive, 0-based |
| | `chunk_index` / `section_heading` / `heading_path` | as above |
| `sentence` | `sentences_per_chunk` / `actual_sentence_count` | `int` |
| | `source_paragraph_index` | `int` |
| | `chunk_index` / `section_heading` / `section_level` / `heading_path` | as above |
| `page_aware` | `page_break_type` | `str` — `heading_boundary`, `estimated`, `explicit`, `rendered`, `section`, `slide_boundary` |
| | `paragraph_count` / `chunk_index` | `int` |
| | `section_heading` / `section_level` / `heading_path` | as above |

#### Keys that are absent, not null

Verified over the fixture corpus at default parameters. In each case the key is
**missing from the map entirely**, so `chunk["metadata"]["actual_sentence_count"]`
raises `KeyError` — use `.get()`.

| Mode | Key(s) | Absent on |
| --- | --- | --- |
| `sentence` | `sentences_per_chunk`, `actual_sentence_count`, `source_paragraph_index` | every pass-through chunk — `content_type` `heading`, `table` or `code_block`. Only `sentence` chunks carry them. (`bullet_list` chunks keep them, with `actual_sentence_count: 0`.) |
| `sentence` | `section_level` | `sentence` chunks in **HTML**, **TXT** and **EPUB** |
| `section` | `char_count` | `heading` pass-through chunks in **HTML** and **EPUB** |
| `page_aware` | `section_level` | chunks whose `content_type` is `page_aware`, in **HTML**, **TXT** and **EPUB** (the `heading` chunks in those same documents do carry it) |

Every other key in the table above was present on every chunk of every pipeline
format tested. See [`sentence` mode](/docs/chunking-modes/sentence#metadata) for
the pass-through rule in context.

  Only the derived modes carry it. In `default` / `structural` the pipeline
  emits `section_heading`, `section_level` and `document_metadata` and nothing
  positional — use the index of the list you were given.

### Two variants

`semantic` and `sliding_window` come in two flavours inside the pipeline:

| | Formats | Adds |
| --- | --- | --- |
| **`semantic`, block variant** | PDF, MD, RTF, JSON family, EML, MBOX, MSG, ODT, ODP, IPYNB | `block_types` (`list[str]`), `has_list` (`bool`), `avg_block_length` (`int`) — TXT gets `avg_block_length` only |
| **`semantic`, prose variant** | HTML, EPUB | `merge_reason` (`str`, duplicates `primary_merge_reason`), `has_body_content` (`bool`); chunks that were passed through unmerged instead carry `page_number` + `footnotes_captions` |
| **`sliding_window`, block variant** | PDF, MD, RTF, JSON family, EML, MBOX, MSG, ODT, ODP, IPYNB | `block_count` (`int`) |
| **`sliding_window`, prose variant** | HTML, TXT, EPUB | `paragraph_count` (`int`) |

### Per-format additions

On top of the core, in **all** modes unless noted.

| Format | Extra keys |
| --- | --- |
| **PDF** | none. `section_level` in `default`/`structural`. |
| **Markdown** | none. `section_level` in `default`/`structural`. |
| **HTML** | `default`/`structural`: `page_number`, `footnotes_captions` (`list`), `heading_path` — and **no** `section_level` |
| **TXT** | `default`/`structural`: `page_number`, `footnotes_captions` — and no `section_level`, no `heading_path` |
| **RTF** | none beyond the core |
| **JSON / JSONL / NDJSON** | `record_range` (`[int, int]`, inclusive, 0-based) |
| **EPUB** | `spine_index` (`int`), `href` (`str`), `is_navigation` (`bool`); `default`/`structural` also `page_number`, `footnotes_captions`, `heading_path` and no `section_level` |
| **IPYNB** | none beyond the core |
| **EML / MSG** | none per chunk — everything is in `document_metadata` |
| **MBOX** | per chunk: `message_index`, `message_subject`, `message_from`, `message_date`, `message_id`, `in_reply_to` (`list`), `references` (`list`) — absent on the mailbox title chunk |
| **ODT** | none beyond the core |
| **ODP** | `slide_number` (`int`), `slide_title` (`str \| null`) |

  This surprises everyone. The key exists on **DOCX** (`default`/`structural`
  and `page_aware`), on **DOC** and on **PPT**. HTML, TXT and EPUB emit it in
  `default`/`structural` but it is `null` for every chunk of every fixture in
  the corpus. PDF never emits it: a PDF chunk locates itself with
  `section_heading` / `heading_path`, and `document_metadata.total_pages` tells
  you how long the document is. If you need per-page PDF chunks, use
  `page_aware` and rely on chunk order, or the positional image names.

### document_metadata by format

The one nested object. Its shape is a union, not a struct — read the keys you
need with a default.

| Format | `document_metadata` keys |
| --- | --- |
| PDF | `source_type`, `total_pages` |
| MD / HTML / TXT | `source_type`; the derived modes add `total_input_blocks` |
| RTF | `source_type`, `title` (`str \| null`), `author` (`str \| null`) — read from the `\info` group |
| JSON family | `source_type`, `record_count`, `top_level` (`"array"`/`"object"`/`"lines"`), `envelope_key` (`str \| null`) |
| EPUB | `source_type`, `title`, `language`, `creator`, `identifier`, `epub_version`, `spine_count`, `toc` (`list` of `{title, href}`), plus plurals `creators`, `identifiers`, `publishers`, `contributors`, `subjects`, `languages` |
| IPYNB | `source_type`, `nbformat` (`str`), `kernel` (`str \| null`), `language` (`str \| null`), `cell_count`, `code_cell_count`, `markdown_cell_count` |
| EML | `source_type`, `subject`, `from`, `to` (`list`), `cc`, `bcc`, `date`, `message_id`, `in_reply_to` (`list`), `references` (`list`), `has_attachments`, `attachment_count` |
| MBOX | `source_type`, `message_count` |
| MSG | `source_type`, `message_class`, `subject`, `from`, `to`, `cc`, `bcc`, `sent_date`, `received_date`, `importance`, `conversation_topic`, `has_attachments`, `attachment_count` |
| ODT | `source_type: "odt"`, `title`, `creator` |
| ODP | `source_type: "odp"`, `title`, `creator`, `slide_count` |

  `.doc`, the spreadsheet family and CSV/TSV emit **no** `document_metadata` at
  all, and DOCX's `default`/`structural` object is the one that carries no
  `source_type`.

### What these formats produce

Real output, so the keys above have something to attach to.

**JSON / JSONL / NDJSON** — one chunk per record, rendered as `key: value`
lines with nested objects indented, `content_type: "bullet_list"`:

```json
// elastic_products.ndjson, default
{ "content": "description: Latest flagship smartphone with titanium design…\nprice: 999.99\nproduct_name: iPhone 15 Pro",
  "content_type": "bullet_list",
  "metadata": { "record_range": [0, 0], "section_heading": null, "section_level": 0,
                "document_metadata": { "source_type": "ndjson", "top_level": "lines",
                                       "record_count": 25, "envelope_key": null } } }
```

`record_range` is `[first, last]`, inclusive and 0-based. Element-level modes
(`default`, `structural`, `sentence`) give a point range like `[3, 3]`; grouping
modes partition the file into contiguous non-overlapping ranges; `sliding_window`
ranges overlap by the window's overlap. The chunk's own record count is
`last - first + 1` — there is deliberately no chunk-level `record_count`,
because `document_metadata.record_count` already means the whole file.
`envelope_key` names the array a document was unwrapped from (`"features"` for
GeoJSON) and is `null` when the document *is* the record set. A 50k-line log
becomes 50k independently embeddable chunks — pair it with
[streaming](/docs/streaming).

**EML / MBOX / MSG** — the subject becomes a `heading` chunk, the body
`plain_paragraph` chunks; the parser is MIME- and encoding-aware.

```text
cpython_msg_01_plain.eml → 2 chunks
  [heading]          This is a test message
  [plain_paragraph]  From: John X. Doe bbb@ddd.com / To: bbb@zzz.org / Date: … / Hi, …

mimekit_simple.mbox → 18 chunks
  [heading]          Mailbox — 3 messages          (no message_index)
  [heading]          Message 1                     message_index: 1
  [heading]          this'll probably break a lot of mime parsers
  [plain_paragraph]  Hello world. …
```

An `.mbox` is many messages in one file, so its identity keys sit on **each
chunk** rather than in `document_metadata` — "which message is this from?" is a
lookup, not a string-parse. `in_reply_to` and `references` are lists of
message-ids, so a thread can be rebuilt without re-parsing the source.

  The chunk output above is real. `<bbb@ddd.com>` survives because CommonMark
  calls that an **autolink**, not raw HTML — as it does `<https://example.com>`.
  Both reach `get_chunks` as their own text. (This was a defect until 0.6.3: the
  pipeline classified them as tags and deleted them, losing 34 addresses across
  `.eml`, 210 across `.mbox` and 10 across `.msg`.)

  **Text that is neither an autolink nor a real tag is still removed**, and for
  `.md` that is correct CommonMark: `Use <Placeholder> and <bbb@ddd.com> and
  generic<T> here.` chunks as `Use  and bbb@ddd.com and generic here.` — the
  address is kept, `<Placeholder>` and `<T>` are not, because the specification
  says `<…>` is raw inline HTML. Plan for it if you write about `<T>`,
  `<placeholder>` or `<your-api-key>`: escape them (`\<T\>`), fence them as
  code (`` `<T>` ``), or read the text from `get_markdown`.

  This applies to the formats that render through the shared Markdown pipeline —
  `.md`, `.eml`, `.mbox`, `.msg`, `.odt`, `.odp`, `.rtf`,
  `.json`/`.jsonl`/`.ndjson`, `.ipynb`, `.pdf` and `.epub`. DOCX, TXT, HTML and
  the spreadsheet family use separate chunkers and never had the behaviour.
  **The metadata is unaffected either way** — `document_metadata.from` reads
  `"John X. Doe <bbb@ddd.com>"` in full.

**RTF** — headings come from paragraph *styles*, not inline markup, so a
converter that flattens the style sheet yields prose chunks with no heading
structure rather than guessed ones. Decoding is code-page correct (cp1252,
Shift-JIS, …), character formatting survives into the markdown, and list markers
are normalised to their *kind* (the glyph a writer painted for a bullet is only
meaningful in its own symbol font). Underline has no markdown equivalent and is
not represented.

```json
// conv_libreoffice_heading123.rtf, default
{ "content": "First paragraph", "content_type": "heading",
  "metadata": { "section_heading": null, "section_level": 1,
                "document_metadata": { "source_type": "rtf",
                                       "title": null, "author": "Paolo Mottadelli" } } }
```

`title` and `author` are deliberately `null` rather than a guess when the writer
stored the value as a `\upr` pair, whose ANSI half is a lossy `?????` copy.

**EPUB** — chunked per spine item, in reading order.

```json
// epubcheck_valid.epub, default
{ "content": "Loomings", "content_type": "heading",
  "metadata": { "spine_index": 0, "href": "OPS/content_001.xhtml", "is_navigation": false,
                "page_number": null, "footnotes_captions": [], "heading_path": null,
                "section_heading": null, "document_metadata": { "…": "see above" } } }
```

Dublin Core elements are repeatable and several fixtures use that — the
**plural** keys carry every value, the singular ones keep the first. `toc` is the
book's own table of contents as `{title, href}` in reading order, read from
`nav.xhtml` (EPUB 3) or `toc.ncx` (EPUB 2), so you get chapter titles even when
the XHTML uses no heading tags. `is_navigation` marks a chunk that came from the
TOC page rather than the content — a link list chunks as ordinary prose
otherwise and pollutes retrieval.

**IPYNB** — markdown cells become prose chunks; **code cells and their outputs
become separate `code_block` chunks**, fenced and kept whole:

```json
// nbc_helloworld.ipynb, default → 2 chunks
{ "content": "```\nprint(\"Hello World\")\n```", "content_type": "code_block",
  "metadata": { "section_heading": null, "section_level": 0,
                "document_metadata": { "source_type": "ipynb", "nbformat": "4.0",
                                       "kernel": null, "language": null,
                                       "cell_count": 1, "code_cell_count": 1,
                                       "markdown_cell_count": 0 } } }
```

**ODT / ODP** — the OpenDocument family mirrors its OOXML counterpart: `.odt`
behaves like DOCX, `.odp` like PPTX (one `heading` chunk titled `Slide N` per
slide, then its body), `.ods` follows the XLSX family below.

```json
// odftoolkit_SlideTest1.odp, default
{ "content": "Slide 1", "content_type": "heading",
  "metadata": { "slide_number": 1, "slide_title": "This is the test documents for ODF toolkit",
                "section_heading": null, "section_level": 2,
                "document_metadata": { "source_type": "odp", "title": null,
                                       "creator": "hanbiao", "slide_count": 1 } } }
```

ODP has no title element the way PPTX does, so `slide_title` is the slide's
first line of text — what a reader would call its title.

## DOCX

| Mode | Keys |
| --- | --- |
| `default` / `structural` | `section_heading` (`str \| null`), `section_heading_level` (`int \| null`), `page_number` (`int`), `footnotes` (`list` of `{id, text}`), `endnotes` (`list`), `document_metadata` (`header_text`, `footer_text`, `image_count` — **no `source_type`**) |
| `section` | `section_heading` (`str`), `section_heading_level`, `section_level`, `heading_path` (**a `str` here, not a list**), `document_metadata` |
| `semantic` | `merge_reason` (`str`, singular — [values](#enumerated-values)), `paragraph_count`, `section_heading`, `section_heading_level`, `document_metadata` |
| `sliding_window` | `window_index`, `window_size`, `overlap`, `paragraph_indices` (`list[int]`), `paragraph_meta` (`list` of `{is_heading, …}`), `heading_count`, `list_item_count`, `document_metadata` |
| `sentence` | `sentences_per_chunk`, `actual_sentence_count`, `chunk_index`, `source_paragraph_index`, `source_paragraph_is_heading`, `source_paragraph_heading_level`, `source_paragraph_is_list`, `source_paragraph_is_table`, `document_metadata` |
| `page_aware` | `page_number`, `page_break_type`, `paragraph_count`, `headings` (`list` of `{level, text}`), `list_item_count`, `table_count`, `section_heading_level`, `document_metadata`; **`chunk_index` + `total_chunks` only on the parts of a split page** |

  DOCX emits **no** `chunk_index` in `section`, `semantic` or `sliding_window` —
  the only pipeline-adjacent format that doesn't. And in `page_aware`,
  `chunk_index`/`total_chunks` mean "part *i* of *n* **of this page**", not a
  document position: `lists.docx` returns seven page chunks, of which exactly
  two carry them.

## DOC and PPT (97–2003)

Both legacy readers emit the **same key set in every mode** — the mode changes
how text is grouped, not what is described.

| Format | Keys (all 7 modes) |
| --- | --- |
| **DOC** | `source` (absolute path), `chunk_index`, `total_chunks`, `paragraph_type` (`heading` / `normal` / `table` / `list_item` / `image`), `heading_level` (1–9 or `null`), `page_number` (`int \| null`), `section_heading`, `section_heading_level`, `heading_path` (`"Chapter > Section"`), `list_level` (0-based, list chunks only), `table_rows` / `table_columns` / `table_cells` (table chunks only). **No `document_metadata`.** |
| **PPT** | the same, minus `list_level`/`table_*`, plus `slide_number`, `slide_title`, and `page_number` carrying the same value as `slide_number`; `document_metadata` is `{source_type, total_slides}` |

`.doc` `page_number` counts the **hard** page breaks the file declares and is
`null` when it declares none — Word recomputes soft pagination at render time
and does not store it.

## PPTX

| Mode | Keys |
| --- | --- |
| `default` / `structural` | `slide_number` (`int`), `slide_range` (`[int, int]`), `slide_title` (`str \| null`), `section_heading`, `document_metadata` (`source_type`, `total_slides`) |
| `section` | `section_heading` (`str`), `slide_range`, `slide_count`, `split_part` / `split_total` (`int \| null`), `document_metadata` |
| `semantic` | `merge_reasons` (`list`), `primary_merge_reason`, `merge_reason` (duplicate of the primary), `keyword_density`, `has_body_content` (`bool`, not always present), `slide_range`, `slide_count`, `slide_title`, `section_heading`, `chunk_index`, `document_metadata` |
| `sliding_window` | `window_index`, `window_size`, `overlap`, `slide_range`, `slide_count`, `truncated` (`bool`), `chunk_index`, `document_metadata` |
| `sentence` | `sentences_per_chunk`, `actual_sentence_count`, `source_slide` (`int`), `slide_range`, `chunk_index`, `document_metadata` |
| `page_aware` | `page_break_type` (`"slide_boundary"`), `slides_per_chunk` (`int`), `slide_range`, `slide_count`, `chunk_index`, `document_metadata` |

  PPTX names the span `slide_range` — a two-element `[first, last]` — in every
  mode, including `page_aware`. There is no `slide_numbers` key anywhere in the
  engine.

## Spreadsheets (XLSX / XLS)

Identical for the whole family — `.xlsm`, `.xlsb`, `.xltx`, `.xltm` and `.ods`
included. No `document_metadata`; provenance is per chunk. `chunk_index` and
`skipped_sheets` are present in **every** mode.

| Mode | `content_type` | Keys |
| --- | --- | --- |
| `row` | `row_document` | `sheet_name` (`str`), `sheet_index` (`int`), `row_index` (`int`), `header_row` (`list[str]`), `col_count`, `rows_per_chunk`, `actual_row_count`, `chunk_index`, `skipped_sheets` |
| `table` | `table_region` | `table_name` (`str \| null`), `is_named_table` (`bool`), `start_row` / `end_row` / `start_col` / `end_col`, `row_count`, `col_count`, `header_row`, `is_split` (`bool`), `split_part` (`int \| null`), `sheet_name`, `sheet_index`, `chunk_index`, `skipped_sheets` |
| `sheet` | `sheet` | `row_count`, `col_count`, `header_row`, `has_named_tables` (`bool`), `named_tables` (`list`), `is_split`, `split_part`, `sheet_name`, `sheet_index`, `chunk_index`, `skipped_sheets` |
| `sliding_window` | `row_window` | `window_index`, `window_size`, `overlap`, `start_row`, `end_row`, `actual_row_count`, `header_row`, `col_count`, `sheet_name`, `sheet_index`, `chunk_index`, `skipped_sheets` |
| `page_aware` | `sheet_region` | `has_print_area` (`bool`), `print_area_ref` (`str \| null`), `region_index`, `start_row` / `end_row` / `start_col` / `end_col`, `row_count`, `col_count`, `header_row`, `is_split`, `split_part`, `sheet_name`, `sheet_index`, `chunk_index`, `skipped_sheets` |
| `semantic` | `semantic_group` | `category_column` (`int \| null`), `category_value` (`str \| null`), `used_fallback` (`bool`), `low_grouping_quality` (`bool`), `avg_group_size` (`float`), `group_index`, `start_row`, `end_row`, `actual_row_count`, `header_row`, `col_count`, `sheet_name`, `sheet_index`, `chunk_index`, `skipped_sheets` |

  `.xls` cannot expose named tables or print areas: in `table` mode
  `is_named_table` is always `false` (heuristic bounding-box detection is used),
  in `sheet` mode `has_named_tables` is always `false` with an empty
  `named_tables`, and `page_aware` always falls back to the full sheet
  (`has_print_area: false`). Every other mode is identical to `.xlsx`.

  `skipped_sheets` lists any sheets calamine could not read (chart sheets, XLM
  macro sheets, corrupt parts). It is an empty list when nothing was dropped, so
  its absence never has to be interpreted. A workbook where *no* sheet is
  readable raises instead.

## CSV / TSV

| Mode | `content_type` | Keys |
| --- | --- | --- |
| `row` / `default` / `page_aware` | `row_group` | `row_start`, `row_end`, `row_count`, `col_count`, `header_row` (`list[str]`), `has_header` (`bool`), `delimiter_detected` (`str`), `encoding` (`str`), `chunk_index` |
| `sliding_window` | `row_window` | `window_index`, `window_size`, `overlap`, `row_start`, `row_end`, `actual_row_count`, `col_count`, `header_row`, `has_header`, `delimiter_detected`, `encoding`, `chunk_index` |

CSV has no way to declare whether its first row is a header, so the engine
infers it: a non-numeric cell above a numeric column reads as a label, and in a
textual column a first cell whose length differs from the rest reads as a label.

- **`has_header: true`** — `header_row` came from the file, and that row is not
  emitted as data.
- **`has_header: false`** — the file is headerless. Every row is emitted as
  data and `header_row` holds synthetic `Column 1 … Column N` labels.

A single-row file is always treated as data, never as a lone header, so it still
produces a chunk.

## Image chunks

With `list_images` on, `content_type: "image"` chunks carry a deliberately small
and **non-uniform** key set. (Without `list_images`, an `image` chunk is a
structural placeholder with no `image_name` at all — see
[Output Schema](/docs/output-schema#image-chunks).)

| Format | Keys |
| --- | --- |
| DOCX family | `image_name`, `alt_text` |
| PDF | `image_name` |
| XLSX family (incl. ODS, XLSB) | `image_name`, `alt_text`, `sheet_name`, `sheet_index` |
| PPTX family | `image_name`, `alt_text` (`str \| null`), `slide_number`, `document_metadata` |
| HTML | `image_name`, `alt_text`, `document_metadata` |
| EPUB | `image_name`, `href` (the manifest path) |
| DOC | `image_name`, `paragraph_index` (`int \| null` for floating images), `paragraph_type: "image"`, plus DOC's usual block |
| PPT | `image_name`, `page_number` (1-based slide, `null` if unattributable), `paragraph_type: "image"`, plus PPT's usual block |
| IPYNB · EML / MBOX / MSG · ODT / ODP | `image_name` |

**Naming.** DOCX, PPTX, XLSX, HTML, ODT/ODP and the legacy DOC/PPT path use a
content hash — `"<16 hex>.<ext>"`, FNV-1a 64-bit, pinned in the engine so the
name is identical in Python, JavaScript and Rust. **PDF is positional**
(`image_p3_1.png`) and always re-encoded to `.png`. **EPUB, IPYNB and the email
formats keep the source name** (`7433694763631080598_cover.jpg`,
`output_image_1.png`, `msg1_image_1.gif`, `testPNG.png`).

## Enumerated values

**`primary_merge_reason`** (pipeline `semantic`, and PPTX) — the winning signal:
`keyword_overlap`, `short_paragraph`, `reference_continuity`, `elaboration`,
`example`, `cause_effect`, `contrast_continuation`, `question_answer`,
`definition_expansion`, `list_continuation`, `heading_merge`, `heading`
(HTML/EPUB), `section_divider` and `single_unit` (PPTX), plus two "nothing
fired" values: `structural_boundary` and `initial` (a singleton chunk).
`merge_reasons` holds every signal that fired and is empty for those two.

**DOCX `merge_reason`** (singular, DOCX only) — `keyword_overlap`,
`short_paragraph`, `heading_merge`, `size_limit`, `reference_continuity`,
`list_continuation`, `docx_heading`, `transition_break`. `size_limit` marks a
chunk closed because the next paragraph would exceed the 1,500-character cap;
`list_continuation` marks a run of list items held together as one unit.

**`page_break_type`** — `heading_boundary`, `estimated`, `explicit`, `rendered`,
`section` (DOCX), `slide_boundary` (PPTX).

**`block_types`** — `heading`, `paragraph`, `list`, `table`, `code_block`.

  HTML, EPUB and PPTX also emit `merge_reason` as a duplicate of
  `primary_merge_reason`; PDF, MD, TXT, RTF, JSON, ODT/ODP, IPYNB and the email
  formats emit only the list plus the primary. See
  [Chunking Modes → semantic](/docs/chunking-modes/semantic).
