A telecom operator in Madrid signs a service-level agreement (SLA) for a corporate MPLS network. The annex contains this table:
| Concepto | Valor |
|---|---|
| Disponibilidad mensual garantizada | 99,95 % |
| Latencia máxima (ida y vuelta) | 50 ms |
| Penalización por hora de indisponibilidad | 1.234,56 € |
Six months later the customer asks whether the provider met its obligations and how much it is owed.
To answer, a system has to read the three values. In Spain they mean
99.95 percent, 50 milliseconds and 1,234.56 euros. A system that assumes
US conventions reads them differently. It stores 1.234,56
as 1.23 euros, and it reads 99,95 either as invalid or as
9,995, depending on how it handles the comma.
No error is raised. The penalty is stored a thousand times too small, and the knowledge graph treats it as a fact.
The model did not invent anything here. The pipeline read the document, extracted the value, typed it as a number and stored it. Each step completed normally, and the result is wrong.
The example is Spanish, but the problem is not. Most of Europe and
Latin America write 1.234,56, France writes
1 234,56, India writes 12,34,567, and the US
writes 1,234.56. Any system that reads contracts from more
than one country has to handle all of them.
Why a misread number is worse than a missing one
When retrieval misses a fact, the gap is usually visible. The answer says the penalty clause was not found, or a verifier marks the claim as unsupported.
A misread number is stored as evidence. If the answer says “the penalty is €1.23 per hour”, a verifier that checks the claim against the stored value finds a match and approves it. The verifier works as designed, but the value it checks against is wrong.
Earlier posts in this series argued that a confidence score is not verification and that each claim must be checked against evidence. This post adds the second part: verification is only as reliable as the evidence it uses. If ingestion corrupts a number, every later check inherits the error.
Why guessing the convention fails
One approach is to guess which convention a document uses. Each guessing method fails in some cases.
Guessing from the number. 1.234,56 has
two separators, so the last one must be the decimal point.
1.250 has one separator followed by three digits. That can
be a thousands separator, or a value with three decimals such as
1.250 kg, 0.125 or 3.459 €/L. The
text alone does not decide it, and SLAs contain many values of this
form.
Guessing from the language. Spanish does not imply a
comma decimal. Mexico writes Spanish with US-style numbers, for example
$1,250.00 MXN, and so does much of Central America. Mapping
Spanish to comma decimals would misread Mexican contracts.
Guessing from the rest of the document. This means finding an unambiguous number and reading the ambiguous ones the same way. It fails when a document has no unambiguous number, and the same clause can then be read differently in two files.
We found the problem in our own code. A pre-production review of
LAKEer’s multilingual support found that our currency extractor read
€1.234,56 as 1.23, with no error and full
confidence. No customer data was affected, because no non-English tenant
had been onboarded yet.
Figure 1 shows how each part of LAKEer read 1.234,56.
The old currency extractor stored 1.23, a conversion step
stored 1.23456, and the table loader left the column as
text. Only a strict parser that follows published locale rules gives a
defined answer in every locale.
Figure 1. Five readers of 1.234,56
| Reader | US en_US | Mexico es_MX | Spain es_ES | Brazil pt_BR | Germany de_DE | France fr_FR | India en_IN |
|---|---|---|---|---|---|---|---|
| Old currency extractor | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 |
| Old value conversion | 1.23456 | 1.23456 | 1.23456 | 1.23456 | 1.23456 | 1.23456 | 1.23456 |
| Old table loader | text | text | text | text | text | text | text |
| Locale-blind fix | 1234.56 | 1234.56 | 1234.56 | 1234.56 | 1234.56 | 1234.56 | 1234.56 |
| Strict CLDR | rejected | rejected | 1234.56 | 1234.56 | 1234.56 | rejected | rejected |
Wrong, stored silently Right Not a number Guessed: no rule says so Strict CLDR
The same text under seven locales. Each row is one way LAKEer has read it. Only the strict CLDR row follows a published rule in every column.
Table columns had the same problem. A column of European-style
thousands (1.234, 12.000) was read as
decimals, so sums and averages over it were a thousand times too
small.
Across 147 readings of common number formats in seven locales (US,
Mexico, Spain, Brazil, Germany, France and India), the old parser
returned 19 wrong values without an error. Every −15
written with a typographic minus lost its sign, and every Spanish
1.234 was stored as 1.234.
Figure 2. What the old parser stored, with no error
| In the document | US en_US | Mexico es_MX | Spain es_ES | Brazil pt_BR | Germany de_DE | France fr_FR | India en_IN |
|---|---|---|---|---|---|---|---|
| −15 | 15 (≠−15) | 15 (≠−15) | 15 (≠−15) | 15 (≠−15) | 15 (≠−15) | 15 (≠−15) | 15 (≠−15) |
| 1.234 | 1.234 | 1.234 | 1.234 (≠1234) | 1.234 (≠1234) | 1.234 (≠1234) | 1.234 | 1.234 |
| 1,234 | 1234 | 1234 | 1234 (≠1.234) | 1234 (≠1.234) | 1234 (≠1.234) | 1234 (≠1.234) | 1234 |
| 3.459 | 3.459 | 3.459 | 3.459 (≠3459) | 3.459 (≠3459) | 3.459 (≠3459) | 3.459 | 3.459 |
| 1 234,56 (space) | 1 | 1 | 1 | 1 | 1 | 1 (≠1234.56) | 1 |
| 12 345 (space) | 12 | 12 | 12 | 12 | 12 | 12 (≠12 345) | 12 |
Correct Wrong, stored silently (intended value in brackets) Two numbers in a non-space locale
Each cell is what the old parser stored for that text under that locale. Red cells are wrong values stored without an error. The intended value is in brackets.
What large platforms do
We looked at how systems that ingest numbers at scale handle this. None of them guesses.
- Amazon’s Selling Partner API accepts only machine
format: a
.decimal, no grouping and an ISO currency code. For inventory spreadsheets prepared by people, the marketplace sets the locale. A file with the wrong separator gets an upload error and is not reinterpreted. - Google Merchant Center and Meta Commerce
Manager require
.decimals with no commas in product feeds. - Excel and Power BI let the user declare a locale when changing a column’s type, then parse strictly under it.
In each case, machine-generated data uses one locale-free format, and data prepared by people is read strictly under one declared locale. Values that do not fit are rejected, not reinterpreted.
What LAKEer does now
We adopted the same approach, using the locale rules of the Unicode Common Locale Data Repository (CLDR), which Java, .NET, the major browsers and most spreadsheet software also use. The design has five rules.
1. Two input channels. Machine-produced values, such
as the extraction model’s output, JSON and APIs, are read in one
canonical format: . decimal and no grouping. Numbers
written by people, in document text and table cells, are parsed strictly
under the tenant’s locale.
2. The locale is declared. A locale names a language
and a country: es_ES for Spain, es_MX for
Mexico. LAKEer can recommend one, but an administrator confirms it
before the first document is ingested. Only the customer knows whether
the documents come from Madrid or Monterrey.
3. Rejected values are kept. Text that does not fit the declared locale keeps its original text and is flagged as unparsed, and each build reports the count. Amazon can return a spreadsheet to the seller, but a signed contract cannot be returned to its author, so the rejection goes to the administrator.
4. Source text is stored with each number. A claim
such as “the penalty is €1,234.56 per hour” can then be checked against
the characters 1.234,56 € in the contract, and a wrong
locale can be found and corrected later.
5. The locale applies only to reading. After parsing, numbers are stored in canonical form. Table columns become numeric columns and calculations do not depend on the locale.
The same separation applies to answers. When a Spanish contract is
queried in English, a language model that rewrites
1.234,56 € as €1,234.56 can introduce a
thousand-fold error in the other direction. So numbers in answers are
formatted by code from the stored value, and verification compares
values, not text. This part is designed but not yet built; today an
answer shows the number as the model writes it.
The strict parser rejects more values than a lenient one. On the same 147 readings it accepted 61, rejected 86 and misread none. A rejected value is reported. A misread value stays hidden until someone notices a wrong result.
Figure 3 shows how strict the published rules are. Spain, Brazil and
Germany reject 1234.56, France rejects
1,234.56, and India rejects 1,234,567 because
it groups digits in lakhs and crores. The only ambiguous row is
1.234, and the declared locale decides it.
Figure 3. What strict CLDR accepts, locale by locale
| In the document | US en_US | Mexico es_MX | Spain es_ES | Brazil pt_BR | Germany de_DE | France fr_FR | India en_IN |
|---|---|---|---|---|---|---|---|
| 1234.56 | 1234.56 | 1234.56 | rejected | rejected | rejected | 1234.56 | 1234.56 |
| 1,234.56 | 1234.56 | 1234.56 | rejected | rejected | rejected | rejected | 1234.56 |
| 1.234,56 | rejected | rejected | 1234.56 | 1234.56 | 1234.56 | rejected | rejected |
| 1.234 | 1.234 | 1.234 | 1234 | 1234 | 1234 | 1.234 | 1.234 |
| 3,5 | rejected | rejected | 3.5 | 3.5 | 3.5 | 3.5 | rejected |
| 1,234,567 | 1 234 567 | 1 234 567 | rejected | rejected | rejected | rejected | rejected |
| 12,34,567.89 | rejected | rejected | rejected | rejected | rejected | rejected | 1 234 567.89 |
| 1 234,56 (space) | rejected | rejected | rejected | rejected | rejected | rejected | rejected |
| 1 234,56 (narrow nbsp) | rejected | rejected | rejected | rejected | rejected | 1234.56 | rejected |
| −15 (typographic minus) | rejected | rejected | rejected | rejected | rejected | rejected | rejected |
Accepted, same value everywhere Value depends on locale Rejected
Each cell is what a strict CLDR parser returns for that text under that locale. Orange cells show where locales accept the same text but give different values.
One limit remains. A tenant with both Spanish and Mexican contracts will see rejections, because one locale per tenant cannot cover both. Per-document locale detection is on the roadmap. Until then, the per-build count of unparsed values shows the administrator when it is needed.
Knowing a number
A theme of this series is that knowing a fact requires more than a stored value. It also requires knowing where the value came from, what rule produced it, and whether that rule was correct.
Regulation points the same way. The EU AI Act requires providers of high-risk systems to keep records that make outputs traceable (Article 12) and to support their accuracy claims (Article 15). A number stored with its source text, the locale used to read it and a flag when it could not be read is that kind of record. It shows how an answer was produced.
A number in a contract is a simple example. 1.234,56
means 1,234.56 in Spain, Germany or Brazil and is not a valid number
under US rules. A system that answers questions about contract
obligations has to know which reading applies, record how it decided,
and report when it could not decide.
In homage: Aristotle (384–322 BC)
This post owes its central idea to Aristotle, Plato’s student and the founder of the Lyceum in Athens. In On Interpretation, he observed that written marks and spoken words are not the same for everyone, and that a word means something by convention, not by nature. Nothing in the sound or shape of a word ties it to what it stands for. People who share a language agree on that link.
Numbers are no different. The characters 1.234,56 mean
1,234.56 under the convention used in Madrid and are not a valid number
under the one used in Monterrey. The marks are identical; only the
convention differs. So the meaning cannot be read off the text, and a
system that guesses is really guessing which convention the writer used.
That is why LAKEer does not guess. It asks the customer, who knows where
the documents come from, to declare the convention.