diff --git a/docling_core/types/doc/__init__.py b/docling_core/types/doc/__init__.py
index 4625f7402..380ef1fa3 100644
--- a/docling_core/types/doc/__init__.py
+++ b/docling_core/types/doc/__init__.py
@@ -21,6 +21,10 @@
BaseMeta,
BasePrediction,
CodeMetaField,
+ DataPointDirection,
+ DataPointMention,
+ DataPointPrecision,
+ DataPointScale,
DescriptionMetaField,
EntitiesMetaField,
EntityMention,
diff --git a/docling_core/types/doc/common/meta.py b/docling_core/types/doc/common/meta.py
index fd7be4238..2dabaabae 100644
--- a/docling_core/types/doc/common/meta.py
+++ b/docling_core/types/doc/common/meta.py
@@ -1,7 +1,7 @@
"""Metadata models attached to document nodes."""
from enum import Enum
-from typing import Annotated, Any, Final, Optional
+from typing import Annotated, Any, Final, Literal, Optional, Union
from pydantic import (
BaseModel,
@@ -10,6 +10,7 @@
Field,
FieldSerializationInfo,
field_serializer,
+ field_validator,
model_validator,
)
from typing_extensions import Self
@@ -159,10 +160,159 @@ class EntityMention(BasePrediction):
] = None
+DataPointPrecision = Literal["exact", "approximate", "lower_bound", "upper_bound", "range_low", "range_high"]
+"""Valid values for DataPointMention.precision."""
+
+DataPointDirection = Literal["increase", "decrease", "neutral"]
+"""Valid values for DataPointMention.direction."""
+
+DataPointScale = Literal[
+ "hundred",
+ "thousand",
+ "k",
+ "million",
+ "m",
+ "billion",
+ "b",
+ "bn",
+ "trillion",
+ "t",
+ "quadrillion",
+]
+"""Valid values for DataPointMention.scale. Each value maps to a known numeric multiplier."""
+
+
+class DataPointMention(EntityMention):
+ """A quantitative data point mentioned in text, extending EntityMention with a numeric breakdown.
+
+ Inherited fields: text (normalised form), orig (source text), label (category),
+ charspan, confidence, created_by.
+ """
+
+ value: Annotated[
+ float,
+ Field(
+ description="Numeric value as written, before scale application. E.g. 4.0 for '$4B'.",
+ examples=[4.0, 30.0, 3.0],
+ ),
+ ]
+
+ unit: Annotated[
+ Optional[str],
+ Field(
+ description="Dimensional unit, e.g. 'USD', '°C', '%'. ISO 4217 recommended for currencies.",
+ examples=["USD", "°C", "%"],
+ ),
+ ] = None
+
+ scale: Annotated[
+ Optional[DataPointScale],
+ Field(
+ description="Magnitude multiplier from the source text, e.g. 'billion', 'million', 'k'.",
+ examples=["billion", "million", "k"],
+ ),
+ ] = None
+
+ normalized_value: Annotated[
+ Optional[float],
+ Field(
+ description="value * scale_factor in base units, e.g. 4e9 for '$4B'. Derived convenience field.",
+ examples=[4_000_000_000.0, 30.0],
+ ),
+ ] = None
+
+ range_end: Annotated[
+ Optional[float],
+ Field(
+ description=(
+ "Upper bound when the data point expresses a range, e.g. 20.0 for 'between 10 and 20°C'. "
+ "Use with precision='range_low'; value holds the lower bound."
+ ),
+ examples=[20.0],
+ ),
+ ] = None
+
+ display_dp: Annotated[
+ Optional[int],
+ Field(
+ ge=0,
+ description=(
+ "Decimal places as written in the source: 0 for '3%', 1 for '3.0%', 2 for '3.00%'. "
+ "None when not determinable."
+ ),
+ examples=[0, 1, 2],
+ ),
+ ] = None
+
+ precision: Annotated[
+ Optional[DataPointPrecision],
+ Field(
+ description=(
+ "Author's epistemic qualification: 'exact', 'approximate', "
+ "'lower_bound', 'upper_bound', 'range_low', 'range_high'."
+ ),
+ examples=["approximate", "lower_bound"],
+ ),
+ ] = None
+
+ direction: Annotated[
+ Optional[DataPointDirection],
+ Field(
+ description=(
+ "Direction of change for delta/growth values: 'increase', 'decrease', 'neutral'. "
+ "E.g. 'revenue grew 3%' → 'increase'."
+ ),
+ examples=["increase", "decrease"],
+ ),
+ ] = None
+
+ @field_validator("range_end", mode="after")
+ @classmethod
+ def _validate_range_end(cls, v: Optional[float], info: Any) -> Optional[float]:
+ if v is not None and info.data.get("value") is None:
+ raise ValueError("range_end requires value to be set")
+ return v
+
+ @property
+ def scale_factor(self) -> Optional[float]:
+ """Numeric multiplier for scale, or None if absent or unrecognised."""
+ if self.scale is None:
+ return None
+ _SCALE_MAP: dict[str, float] = {
+ "hundred": 1e2,
+ "thousand": 1e3,
+ "k": 1e3,
+ "million": 1e6,
+ "m": 1e6,
+ "billion": 1e9,
+ "b": 1e9,
+ "bn": 1e9,
+ "trillion": 1e12,
+ "t": 1e12,
+ "quadrillion": 1e15,
+ }
+ return _SCALE_MAP.get(self.scale)
+
+ def compute_normalized_value(self) -> Optional[float]:
+ """Return value * scale_factor without caching.
+
+ Returns None when scale is present but unrecognised.
+ Use this to derive or refresh normalized_value after construction.
+ """
+ factor = self.scale_factor
+ if factor is None and self.scale is not None:
+ return None
+ return self.value * (factor if factor is not None else 1.0)
+
+
class EntitiesMetaField(_ExtraAllowingModel):
- """Container for extracted entity mentions."""
+ """Container for extracted entity mentions.
+
+ The mentions list accepts both plain EntityMention objects and the richer
+ DataPointMention subclass; use isinstance(m, DataPointMention) to filter.
+ """
- mentions: Annotated[list[EntityMention], Field(min_length=1)]
+ mentions: Annotated[list[Union[DataPointMention, EntityMention]], Field(min_length=1)]
class KeywordsMetaField(_ExtraAllowingModel):
diff --git a/docs/DoclingDocument.json b/docs/DoclingDocument.json
index 74f4d839d..3182d6670 100644
--- a/docs/DoclingDocument.json
+++ b/docs/DoclingDocument.json
@@ -590,6 +590,266 @@
"title": "CoordOrigin",
"type": "string"
},
+ "DataPointMention": {
+ "additionalProperties": true,
+ "description": "A quantitative data point mentioned in text, extending EntityMention with a numeric breakdown.\n\nInherited fields: text (normalised form), orig (source text), label (category),\ncharspan, confidence, created_by.",
+ "properties": {
+ "confidence": {
+ "anyOf": [
+ {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The confidence of the prediction.",
+ "examples": [
+ 0.9,
+ 0.42
+ ],
+ "title": "Confidence"
+ },
+ "created_by": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The origin of the prediction.",
+ "examples": [
+ "ibm-granite/granite-docling-258M"
+ ],
+ "title": "Created By"
+ },
+ "text": {
+ "description": "Normalized text of the entity mention.",
+ "title": "Text",
+ "type": "string"
+ },
+ "orig": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Exact source text extracted from the original charspan, analogous to TextItem.orig. This may differ from 'text' when the mention has been normalized.",
+ "title": "Orig"
+ },
+ "label": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Entity type or category.",
+ "title": "Label"
+ },
+ "charspan": {
+ "anyOf": [
+ {
+ "description": "Character span (0-indexed)",
+ "maxItems": 2,
+ "minItems": 2,
+ "prefixItems": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "integer"
+ }
+ ],
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Character span (0-indexed) of the entity mention in the source text.",
+ "title": "Charspan"
+ },
+ "value": {
+ "description": "Numeric value as written, before scale application. E.g. 4.0 for '$4B'.",
+ "examples": [
+ 4.0,
+ 30.0,
+ 3.0
+ ],
+ "title": "Value",
+ "type": "number"
+ },
+ "unit": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Dimensional unit, e.g. 'USD', '°C', '%'. ISO 4217 recommended for currencies.",
+ "examples": [
+ "USD",
+ "°C",
+ "%"
+ ],
+ "title": "Unit"
+ },
+ "scale": {
+ "anyOf": [
+ {
+ "enum": [
+ "hundred",
+ "thousand",
+ "k",
+ "million",
+ "m",
+ "billion",
+ "b",
+ "bn",
+ "trillion",
+ "t",
+ "quadrillion"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Magnitude multiplier from the source text, e.g. 'billion', 'million', 'k'.",
+ "examples": [
+ "billion",
+ "million",
+ "k"
+ ],
+ "title": "Scale"
+ },
+ "normalized_value": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "value * scale_factor in base units, e.g. 4e9 for '$4B'. Derived convenience field.",
+ "examples": [
+ 4000000000.0,
+ 30.0
+ ],
+ "title": "Normalized Value"
+ },
+ "range_end": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Upper bound when the data point expresses a range, e.g. 20.0 for 'between 10 and 20°C'. Use with precision='range_low'; value holds the lower bound.",
+ "examples": [
+ 20.0
+ ],
+ "title": "Range End"
+ },
+ "display_dp": {
+ "anyOf": [
+ {
+ "minimum": 0,
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Decimal places as written in the source: 0 for '3%', 1 for '3.0%', 2 for '3.00%'. None when not determinable.",
+ "examples": [
+ 0,
+ 1,
+ 2
+ ],
+ "title": "Display Dp"
+ },
+ "precision": {
+ "anyOf": [
+ {
+ "enum": [
+ "exact",
+ "approximate",
+ "lower_bound",
+ "upper_bound",
+ "range_low",
+ "range_high"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Author's epistemic qualification: 'exact', 'approximate', 'lower_bound', 'upper_bound', 'range_low', 'range_high'.",
+ "examples": [
+ "approximate",
+ "lower_bound"
+ ],
+ "title": "Precision"
+ },
+ "direction": {
+ "anyOf": [
+ {
+ "enum": [
+ "increase",
+ "decrease",
+ "neutral"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Direction of change for delta/growth values: 'increase', 'decrease', 'neutral'. E.g. 'revenue grew 3%' → 'increase'.",
+ "examples": [
+ "increase",
+ "decrease"
+ ],
+ "title": "Direction"
+ }
+ },
+ "required": [
+ "text",
+ "value"
+ ],
+ "title": "DataPointMention",
+ "type": "object"
+ },
"DescriptionAnnotation": {
"description": "DescriptionAnnotation.",
"properties": {
@@ -707,11 +967,18 @@
},
"EntitiesMetaField": {
"additionalProperties": true,
- "description": "Container for extracted entity mentions.",
+ "description": "Container for extracted entity mentions.\n\nThe mentions list accepts both plain EntityMention objects and the richer\nDataPointMention subclass; use isinstance(m, DataPointMention) to filter.",
"properties": {
"mentions": {
"items": {
- "$ref": "#/$defs/EntityMention"
+ "anyOf": [
+ {
+ "$ref": "#/$defs/DataPointMention"
+ },
+ {
+ "$ref": "#/$defs/EntityMention"
+ }
+ ]
},
"minItems": 1,
"title": "Mentions",
diff --git a/test/data/doc/meta/entity_mentions.dt b/test/data/doc/meta/entity_mentions.dt
new file mode 100644
index 000000000..38100bb1f
--- /dev/null
+++ b/test/data/doc/meta/entity_mentions.dt
@@ -0,0 +1,4 @@
+
IBM reported revenue of $16.3B in Q3 2025, up 9% year-over-year.
+ +The population of Switzerland in 2026 is approximately 9 million people.
+ +Operating margins improved to between 18% and 21% during the period.
+ +