> ## Documentation Index
> Fetch the complete documentation index at: https://docs.extend.ai/llms.txt
> Use this file to discover all available pages before exploring further.
>
> ## API version
> The current API version is `2026-02-09`, served at the site root (no version prefix in URLs).
> If this page URL contains `/2025-04-21/` or `/2024-12-23/`, you are reading an older API version.
> Prefer the current docs at https://docs.extend.ai/llms.txt unless the user explicitly needs that older version.
> Do not treat older-version pages as the source of truth for new integrations.

# Parsing Response Format

> Understand the Parse response: top-level fields, chunks, blocks, and bounding-box coordinates.

Parse returns document content in a structure optimized for RAG, LLM context, and citation. Every run gives you both high-level formatted `content` and detailed block-level data with spatial coordinates. This page explains every field in the response.

---

## Response structure

A completed parse run looks like this (truncated to a single chunk and block for brevity). The parsed content lives in `output.chunks`. Each chunk has a formatted `content` string and a `blocks` array of typed, layout-aware elements.

```json
{
  "object": "parse_run",
  "id": "pr_3f1j6I1gsw5k96xFiCnkM",
  "file": {
    "object": "file",
    "id": "file_GzKUy0VDhHscv7tweODYb",
    "name": "bank_statement.pdf"
  },
  "status": "PROCESSED",
  "output": {
    "chunks": [
      {
        "object": "chunk",
        "type": "page",
        "content": "CHASE JPMorgan Chase Bank, N.A. P O Box 659754...",
        "metadata": { "pageRange": { "start": 1, "end": 1 } },
        "blocks": [
          {
            "object": "block",
            "id": "block_WNoJ0WbMj4pRW9MpMpUox",
            "type": "text",
            "content": "CHASE JPMorgan Chase Bank, N.A. P O Box 659754 San Antonio, TX 78265 - 9754",
            "details": {},
            "metadata": { "page": { "number": 1, "width": 612, "height": 792 } },
            "polygon": [
              { "x": 56.873, "y": 35.374 },
              { "x": 162.173, "y": 35.215 },
              { "x": 162.245, "y": 81.158 },
              { "x": 56.938, "y": 81.317 }
            ],
            "boundingBox": { "left": 56.873, "top": 35.215, "right": 162.245, "bottom": 81.317 }
          }
        ]
      }
    ],
    "metadata": {
      "originalMimeType": "application/pdf",
      "finalMimeType": "application/pdf",
      "pages": [
        { "number": 1, "rotationApplied": 0, "originalPageWidth": 612, "originalPageHeight": 792, "dpi": 72 }
      ]
    }
  },
  "metrics": { "pageCount": 7, "processingTimeMs": 8293 },
  "usage": { "credits": 14 }
}
```

### Top-level fields

| Field                              | Type           | Description                                                                                                                                                                                                                                                                                        |
| ---------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `object`                           | string         | Always `"parse_run"`.                                                                                                                                                                                                                                                                              |
| `id`                               | string         | Unique identifier for the run (e.g. `pr_...`). Use it to [fetch results](/api-reference/endpoints/parse/get-parse-run) later.                                                                                                                                                                      |
| `file`                             | object \| null | The parsed file (`id`, `name`). Reusable as input to other endpoints.                                                                                                                                                                                                                              |
| `status`                           | string         | `PENDING`, `PROCESSING`, `PROCESSED`, or `FAILED`.                                                                                                                                                                                                                                                 |
| `output`                           | object \| null | Parsed content. Present when `status` is `PROCESSED` and you did not request a URL response. Contains `chunks` and `metadata` (page rotation, original dimensions, and file type — see [Reconciling coordinates against your original file](#reconciling-coordinates-against-your-original-file)). |
| `outputUrl`                        | string \| null | Presigned URL to download the same `output` shape as JSON. Present only with `responseType=url`. See [Inline output vs. URL](#inline-output-vs-url).                                                                                                                                               |
| `metrics`                          | object         | `pageCount` and `processingTimeMs` for the run.                                                                                                                                                                                                                                                    |
| `usage`                            | object         | Credits consumed (`usage.credits`).                                                                                                                                                                                                                                                                |
| `config`                           | object         | The full configuration used, including defaults that were applied.                                                                                                                                                                                                                                 |
| `failureReason` / `failureMessage` | string \| null | Machine-readable code and human-readable message. Present when `status` is `FAILED`.                                                                                                                                                                                                               |

---

## Inline output vs. URL

By default, parsed content is returned inline in `output`. For large documents, the response can get big, so you can ask for a presigned download URL instead by adding the `responseType=url` query parameter to your parse request.

#### Inline (default)

The content is embedded directly in the response body.

```json
{
  "status": "PROCESSED",
  "output": { "chunks": [ /* ... */ ] },
  "outputUrl": null
}
```

**How to use it:** read `output.chunks` directly.

#### URL (responseType=url)

The response carries a download link instead of inline content.

**How to use it:** fetch the JSON at `outputUrl`. It has the same shape as `output` (a top-level `chunks` array).

```json
{
  "status": "PROCESSED",
  "output": null,
  "outputUrl": "https://extend-files.s3.us-east-2.amazonaws.com/parse-runs/pr_3f1j6I1gsw5k96xFiCnkM/output.json?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&..."
}
```

`outputUrl` expires **15 minutes** after the response is returned. Download or process the content promptly.

Handle both shapes by checking which field is populated:

#### Python

```python
import requests

if response.output_url:
    output = requests.get(response.output_url).json()
else:
    output = response.output

for chunk in output["chunks"]:
    print(chunk["content"])
```

#### TypeScript

```typescript
const output = response.outputUrl
  ? await (await fetch(response.outputUrl)).json()
  : response.output;

for (const chunk of output.chunks) {
  console.log(chunk.content);
}
```

#### Java

```java
ParseRunOutput output;
if (response.getOutputUrl().isPresent()) {
    String json = new String(
        new java.net.URL(response.getOutputUrl().get()).openStream().readAllBytes());
    output = new ObjectMapper().readValue(json, ParseRunOutput.class);
} else {
    output = response.getOutput().get();
}

for (var chunk : output.getChunks()) {
    System.out.println(chunk.getContent());
}
```

#### Go

```go
var output *extend.ParseRunOutput
if response.OutputURL != nil {
	resp, err := http.Get(*response.OutputURL)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	if err := json.NewDecoder(resp.Body).Decode(&output); err != nil {
		log.Fatal(err)
	}
} else {
	output = response.Output
}

for _, chunk := range output.Chunks {
	fmt.Println(chunk.Content)
}
```

---

## Understanding chunks

Chunks are the top-level units in `output.chunks`. Depending on your [chunking strategy](/parsing/configuration), a chunk represents a page (default), a logical section, or the whole document.

```json
{
  "object": "chunk",
  "type": "page",
  "content": "# Account Summary\n\n| Date | Description | Amount |\n| --- | --- | --- |\n...",
  "metadata": { "pageRange": { "start": 1, "end": 1 } },
  "blocks": [ /* ... */ ]
}
```

| Field                                            | Description                                                                                                    |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `object`                                         | Always `"chunk"`.                                                                                              |
| `type`                                           | `page`, `section`, or `document`, based on your chunking strategy.                                             |
| `content`                                        | Fully formatted content for the chunk (markdown by default), ready to drop into an LLM prompt.                 |
| `metadata.pageRange`                             | The `start` and `end` page numbers this chunk covers (equal when the chunk is within a single page).           |
| `metadata.minOcrConfidence` / `avgOcrConfidence` | Lowest and average per-word OCR confidence for the chunk, or `null` when word-level confidence is unavailable. |
| `blocks`                                         | Array of block objects making up the chunk. See [Understanding blocks](#understanding-blocks).                 |

### content vs. blocks

Each chunk gives you two views of the same content. Pick based on what you're building:

* **Use `chunk.content` when** you want ready-to-use formatted text: feeding an LLM, building a RAG index, or displaying a page. Concatenate every chunk's `content` to reconstruct the whole document.
* **Use `chunk.blocks` when** you need structure or position: pulling out only tables or figures, building citations and highlights, or rendering overlays on the original document.

#### Python

```python
def extract_tables(response):
    tables = []
    for chunk in response.output.chunks:
        for block in chunk.blocks:
            if block.type == "table":
                tables.append({
                    "content": block.content,  # markdown / HTML table
                    "rows": block.details.row_count,
                    "columns": block.details.column_count,
                    "page_number": block.metadata.page.number,
                    "position": block.bounding_box,
                })
    return tables
```

#### TypeScript

```typescript
import { Extend } from "extend-ai";

function extractTables(response: Extend.ParseRun) {
  const tables = [];
  for (const chunk of response.output?.chunks ?? []) {
    for (const block of chunk.blocks) {
      if (block.details.type === "table_details") {
        tables.push({
          content: block.content, // markdown / HTML table
          rows: block.details.rowCount,
          columns: block.details.columnCount,
          pageNumber: block.metadata.page?.number,
          position: block.boundingBox,
        });
      }
    }
  }
  return tables;
}
```

#### Java

```java
List<Map<String, Object>> tables = new ArrayList<>();
for (Chunk chunk : response.getOutput().get().getChunks()) {
    for (Block block : chunk.getBlocks()) {
        if (block.getType().equals(BlockType.TABLE)) {
            TableDetails details = (TableDetails) block.getDetails().get();
            Map<String, Object> table = new HashMap<>();
            table.put("content", block.getContent()); // markdown / HTML table
            table.put("rows", details.getRowCount());
            table.put("columns", details.getColumnCount());
            table.put("pageNumber", block.getMetadata().getPage().get().getNumber());
            table.put("position", block.getBoundingBox());
            tables.add(table);
        }
    }
}
```

#### Go

```go
type TableSummary struct {
	Content    string
	Rows       int
	Columns    int
	PageNumber int
	Position   *extend.BoundingBox
}

func extractTables(response *extend.ParseRun) []TableSummary {
	var tables []TableSummary
	for _, chunk := range response.Output.Chunks {
		for _, block := range chunk.Blocks {
			if td := block.Details.GetTableDetails(); td != nil {
				tables = append(tables, TableSummary{
					Content:    block.Content, // markdown / HTML table
					Rows:       td.RowCount,
					Columns:    td.ColumnCount,
					PageNumber: block.Metadata.Page.Number,
					Position:   block.BoundingBox,
				})
			}
		}
	}
	return tables
}
```

---

## Understanding blocks

Blocks are the atomic elements within a chunk: each paragraph, heading, table, figure, and key-value region is its own block, with type-specific `details` and spatial coordinates.

```json
{
  "object": "block",
  "id": "block_WNoJ0WbMj4pRW9MpMpUox",
  "type": "table",
  "content": "| Date | Description | Amount |\n| --- | --- | --- |\n| 01/05 | Direct Deposit | $2,500.00 |",
  "details": { "type": "table_details", "rowCount": 2, "columnCount": 3 },
  "metadata": { "page": { "number": 1, "width": 612, "height": 792 } },
  "polygon": [
    { "x": 61.2, "y": 158.4 },
    { "x": 550.8, "y": 158.4 },
    { "x": 550.8, "y": 356.4 },
    { "x": 61.2, "y": 356.4 }
  ],
  "boundingBox": { "left": 61.2, "top": 158.4, "right": 550.8, "bottom": 356.4 }
}
```

### Block fields

| Field                                            | Type           | Description                                                                                                                                                                                                                                            |
| ------------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `object`                                         | string         | Always `"block"`.                                                                                                                                                                                                                                      |
| `id`                                             | string         | Stable identifier, derived from the block content.                                                                                                                                                                                                     |
| `parentBlockId`                                  | string         | Set only on child blocks (e.g. a table cell points to its table).                                                                                                                                                                                      |
| `type`                                           | string         | The kind of element (see [Block types](#block-types)).                                                                                                                                                                                                 |
| `content`                                        | string         | The block's content in the target format.                                                                                                                                                                                                              |
| `details`                                        | object         | Type-specific extra data (see [Block details](#block-details)).                                                                                                                                                                                        |
| `metadata.page`                                  | object         | `number`, plus page `width` and `height`. For rotation and original-dimension info, see `output.metadata.pages` (matched by `number`) under [Reconciling coordinates against your original file](#reconciling-coordinates-against-your-original-file). |
| `metadata.minOcrConfidence` / `avgOcrConfidence` | number \| null | Per-block OCR confidence, or `null` when unavailable.                                                                                                                                                                                                  |
| `polygon`                                        | array          | Precise outline as `{ x, y }` points. See [Bounding box coordinates](#bounding-box-coordinates).                                                                                                                                                       |
| `boundingBox`                                    | object         | Simplified rectangle: `left`, `top`, `right`, `bottom`.                                                                                                                                                                                                |
| `children`                                       | array          | Nested blocks (e.g. table cells when `cellBlocksEnabled` is set on `parse_performance` `1.0.1`).                                                                                                                                                       |

### Block types

| Type                        | Description                                                                                                                                                                         |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`                      | Body paragraphs and regular text.                                                                                                                                                   |
| `heading`                   | Document or section headings.                                                                                                                                                       |
| `section_heading`           | Subsection headings.                                                                                                                                                                |
| `table`                     | Tabular data (markdown or HTML based on config).                                                                                                                                    |
| `table_head` / `table_cell` | Header and body cells, available as child blocks of a table when `cellBlocksEnabled` is set on `parse_performance` `1.0.1`. Not currently available on `parse_performance` `2.0.0`. |
| `figure`                    | Images, charts, diagrams, or logos.                                                                                                                                                 |
| `key_value`                 | Key-value regions such as form fields.                                                                                                                                              |
| `page_number`               | Page number indicators.                                                                                                                                                             |
| `barcode`                   | Barcodes and QR codes.                                                                                                                                                              |
| `formula`                   | Mathematical formulas and equations.                                                                                                                                                |
| `header` / `footer`         | Page headers and footers.                                                                                                                                                           |

### Block details

The `details` object varies by block type. It is an empty object for plain blocks by default, but some options add type-specific metadata.

| Block type         | `details` fields                                                                                                                              |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `text` / `heading` | `cellReference` (source spreadsheet range) when Excel cell metadata is enabled.                                                               |
| `table`            | `rowCount`, `columnCount`.                                                                                                                    |
| `table_cell`       | `rowIndex`, `columnIndex`; with Excel cell metadata enabled, `cellReference` and `formula`; with Excel cell formatting enabled, `formatting`. |
| `figure`           | `imageUrl` (clipped figure image), `figureType` (`chart`, `image`, `diagram`, `logo`, `other`) when classification is enabled.                |
| `formula`          | `latex` representation of the formula.                                                                                                        |
| `barcode`          | `decodedValue`, `format` (e.g. `QRCode`, `Code128`) when barcode reading is enabled.                                                          |
| `key_value`        | No extra fields.                                                                                                                              |

### Excel cell metadata and formatting

For advanced Excel parsing, `advancedOptions.excelIncludeCellMetadata` adds spreadsheet provenance to block details. Table cells receive `details.cellReference` in A1 notation (for example, `"B2"`, or `"A1:C1"` for a merged cell) and formula cells receive `details.formula` with a leading `=`. Text or heading blocks that come from spreadsheet cells can receive a source range in `details.cellReference`.

If `blockOptions.tables.targetFormat` is `"html"`, the same metadata is also emitted in table markup as `data-cell` and `data-formula` attributes. Markdown output omits those attributes, but the structured block details still carry the metadata.

`advancedOptions.excelIncludeCellFormatting` adds `details.formatting` to table cell blocks when formatting is present. The formatting object can include `bold`, `italic`, `fontColor`, and `backgroundColor`; colors are hex strings such as `"#ff0000"`. For HTML table output, inline cell styles are also preserved.

```json
{
  "type": "table_cell",
  "content": "40",
  "details": {
    "type": "table_cell_details",
    "rowIndex": 1,
    "columnIndex": 1,
    "cellReference": "B2",
    "formula": "=A2*10",
    "formatting": {
      "bold": true,
      "fontColor": "#ff0000",
      "backgroundColor": "#ffff00"
    }
  }
}
```

---

## Confidence

Parse reports OCR confidence so you can flag regions that may need review. Both chunks and blocks expose two aggregated scores in their `metadata`:

| Field              | Description                                                                                                   |
| ------------------ | ------------------------------------------------------------------------------------------------------------- |
| `minOcrConfidence` | The lowest per-word OCR confidence in the chunk or block (0–1). Use it to catch the weakest text in a region. |
| `avgOcrConfidence` | The average per-word OCR confidence across the chunk or block (0–1). A read on overall quality.               |

These reflect **OCR** confidence (how reliably the characters were recognized). Both are `null` when word-level confidence is unavailable.

```json
{
  "object": "chunk",
  "type": "page",
  "metadata": {
    "pageRange": { "start": 1, "end": 1 },
    "minOcrConfidence": 0.71,
    "avgOcrConfidence": 0.98
  }
}
```

A low `minOcrConfidence` could be used as a trigger for routing a page to manual review.

---

## Bounding box coordinates

Before parsing, Extend automatically detects and corrects rotated or skewed pages — straightening a sideways-scanned invoice, for example — because OCR and layout detection are far more accurate on an upright page. As a result, every `boundingBox` and `polygon` below describes the position on this corrected, upright page, which may not match the orientation of the file you originally uploaded. If you need to overlay these coordinates on your own copy of the original file, see [Reconciling coordinates against your original file](#reconciling-coordinates-against-your-original-file).

Every block carries two spatial representations:

* **`polygon`** — the precise outline of the block, as an array of `{ x, y }` points.
* **`boundingBox`** — a simplified, axis-aligned rectangle around the block (`left`, `top`, `right`, `bottom`).

### Coordinate system

Coordinates share the page's coordinate space, with the origin at the **top-left** of the page: `x` increases to the right and `y` increases downward. Every block reports the page's own dimensions at `metadata.page.width` and `metadata.page.height` in the same units, so you can divide by them to express any position as a fraction of the page when you need normalized, resolution-independent values.

```
(0, 0)                                   (pageWidth, 0)
  ┌────────────────────────────────────────────┐
  │   (left, top)                              │
  │      ●───────────────────────┐             │
  │      │                       │             │
  │      │         block         │             │
  │      │                       │             │
  │      └───────────────────────●             │
  │                          (right, bottom)   │
  │                                            │
  └────────────────────────────────────────────┘
(0, pageHeight)                  (pageWidth, pageHeight)
```

| Field                  | Description                                                 |
| ---------------------- | ----------------------------------------------------------- |
| `boundingBox.left`     | The block's left edge, measured from the left of the page.  |
| `boundingBox.top`      | The block's top edge, measured from the top of the page.    |
| `boundingBox.right`    | The block's right edge, measured from the left of the page. |
| `boundingBox.bottom`   | The block's bottom edge, measured from the top of the page. |
| `metadata.page.width`  | Page width, in the same units as the coordinates.           |
| `metadata.page.height` | Page height, in the same units as the coordinates.          |

To express a position as a fraction of the page (0–1), divide each coordinate by the page dimensions: `left / metadata.page.width` and `top / metadata.page.height`.

### Build highlights and overlays

Because coordinates are tied to known page dimensions, you can normalize them to whatever your viewer expects (CSS percentages, image pixels, etc.):

#### Python

```python
def find_highlights(response, search_term):
    highlights = []
    for chunk in response.output.chunks:
        for block in chunk.blocks:
            if search_term not in block.content:
                continue
            page = block.metadata.page
            box = block.bounding_box
            highlights.append({
                "page_number": page.number,
                # Normalized to 0–1 for resolution-independent rendering
                "left": box.left / page.width,
                "top": box.top / page.height,
                "width": (box.right - box.left) / page.width,
                "height": (box.bottom - box.top) / page.height,
            })
    return highlights
```

#### TypeScript

```typescript
import { Extend } from "extend-ai";

function findHighlights(response: Extend.ParseRun, searchTerm: string) {
  const highlights = [];
  for (const chunk of response.output?.chunks ?? []) {
    for (const block of chunk.blocks) {
      if (!block.content.includes(searchTerm)) continue;

      const page = block.metadata.page!;
      const box = block.boundingBox;
      highlights.push({
        pageNumber: page.number,
        // Normalized to 0–1 for resolution-independent rendering
        left: box.left! / page.width!,
        top: box.top! / page.height!,
        width: (box.right! - box.left!) / page.width!,
        height: (box.bottom! - box.top!) / page.height!,
      });
    }
  }
  return highlights;
}
```

#### Java

```java
List<Map<String, Object>> highlights = new ArrayList<>();
for (Chunk chunk : response.getOutput().get().getChunks()) {
    for (Block block : chunk.getBlocks()) {
        if (!block.getContent().contains(searchTerm)) continue;

        BlockMetadataPage page = block.getMetadata().getPage().get();
        BoundingBox box = block.getBoundingBox();
        double width = page.getWidth().get();
        double height = page.getHeight().get();

        Map<String, Object> highlight = new HashMap<>();
        highlight.put("pageNumber", page.getNumber());
        // Normalized to 0–1 for resolution-independent rendering
        highlight.put("left", box.getLeft().get() / width);
        highlight.put("top", box.getTop().get() / height);
        highlight.put("width", (box.getRight().get() - box.getLeft().get()) / width);
        highlight.put("height", (box.getBottom().get() - box.getTop().get()) / height);
        highlights.add(highlight);
    }
}
```

#### Go

```go
type Highlight struct {
	PageNumber               int
	Left, Top, Width, Height float64
}

func findHighlights(response *extend.ParseRun, searchTerm string) []Highlight {
	var highlights []Highlight
	for _, chunk := range response.Output.Chunks {
		for _, block := range chunk.Blocks {
			if !strings.Contains(block.Content, searchTerm) {
				continue
			}
			page := block.Metadata.Page
			box := block.BoundingBox
			highlights = append(highlights, Highlight{
				PageNumber: page.Number,
				// Normalized to 0–1 for resolution-independent rendering
				Left:   *box.Left / *page.Width,
				Top:    *box.Top / *page.Height,
				Width:  (*box.Right - *box.Left) / *page.Width,
				Height: (*box.Bottom - *box.Top) / *page.Height,
			})
		}
	}
	return highlights
}
```

### Reconciling coordinates against your original file

Parse runs report per-page rotation and original-dimension info on **`output.metadata.pages`** — a separate array alongside `chunks`, distinct from each block's `metadata.page`. Match entries by page number: `output.metadata.pages.find(p => p.number === block.metadata.page.number)`. This is set when the file is a PDF or when it was converted to a PDF.

Each entry has:

| Field                                      | Description                                                                                                                                                                                                                                                                                                                                                           |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rotationApplied`                          | Degrees Extend rotated the page **clockwise** to make it upright. `0` means rotation detection ran and the page was already upright; **`null`** means rotation detection was disabled for this run ([`pageRotationEnabled: false`](/2026-02-09/parsing/configuration#advancedoptionspagerotationenabled)) — coordinates need no orientation transform in either case. |
| `originalPageWidth` / `originalPageHeight` | The file's true page size, independent of rendering resolution — **not** yet in the same coordinate space as `boundingBox`/`polygon`.                                                                                                                                                                                                                                 |
| `dpi`                                      | The DPI that `boundingBox`/`polygon` coordinates on this page are scaled to. Multiply `originalPageWidth`/`originalPageHeight` by `dpi / 72` to bring them into that same coordinate space before inverting rotation.                                                                                                                                                 |

Reusing the same file (by URL or file `id`) across multiple runs? Later runs will report `rotationApplied: 0` — rotation is detected once, when the file first enters Extend's system, and the processed file is already upright by the time a later run reads it.

`output.metadata` itself is `null` for parse runs that completed before this field was introduced.

Every `boundingBox` and `polygon` on a page is expressed in the corrected, upright frame — not the orientation of the file you originally uploaded. If you need to draw a citation or highlight on your own copy of the original file, map the returned coordinates back in two steps:

**1. Scale the original page dimensions into the returned coordinate space:**

```
W = originalPageWidth * (dpi / 72)
H = originalPageHeight * (dpi / 72)
```

**2. Invert the rotation.** Given a returned coordinate `(x, y)`:

| `rotationApplied` | `original_x` | `original_y` |
| ----------------- | ------------ | ------------ |
| `90`              | `y`          | `H - x`      |
| `180`             | `W - x`      | `H - y`      |
| `270`             | `W - y`      | `x`          |
| `0` or `null`     | `x`          | `y`          |

Apply the conversion to each corner of a `boundingBox` or each point of a `polygon` individually — rotating the box as a whole (for example, swapping `width` and `height`) isn't equivalent.

**Example:** a page's `originalPageWidth` is 100 and `originalPageHeight` is 200, `dpi` is 150, and `rotationApplied` is 90. First scale: `W = 100 * (150/72) ≈ 208.3`, `H = 200 * (150/72) ≈ 416.7`. Then a returned corner at `(375, 10)` maps back to `(10, 41.7)`: `original_x = y = 10`, `original_y = H - x = 416.7 - 375 = 41.7`.

```javascript
function toOriginalCoordinates(x, y, pageMetadata) {
  const { rotationApplied, originalPageWidth, originalPageHeight, dpi } = pageMetadata;
  const scale = dpi / 72;
  const W = originalPageWidth * scale;
  const H = originalPageHeight * scale;

  switch (rotationApplied) {
    case 90:
      return { x: y, y: H - x };
    case 180:
      return { x: W - x, y: H - y };
    case 270:
      return { x: W - y, y: x };
    default:
      return { x, y };
  }
}
```