chilean-plate-validator
v2.0.0
Published
Lightweight, zero-dependency validator for Chilean license plates (PPU)
Maintainers
Readme
chilean-plate-validator
🇨🇱 Description
A lightweight, zero-dependency utility for Chilean license plate (PPU) validation and identification, built for LPR/OCR pipelines.
Features
- Validated against the official series table. Old-format plates are checked against the 582 two-letter series published by the Registro Civil, not just a
LL·nnnnshape — so the 94 combinations that were never issued are rejected. - OCR-tolerant (fuzzy mode). Corrects visually confusable characters (
8↔B,5↔S,6↔G,7↔T,2↔Z,0↔O/D,1↔I) and returns every valid plate the input could have been. - Regional detection (optional). A separate entry point tells you whether a string looks like a plate from Chile or a neighbouring country.
- Dual build. ESM (
import) and CommonJS (require), with types for both. - Zero runtime dependencies.
Supported formats
Format notation: L = letter, 1 = digit.
| Type | Format | Example | Description |
|----------------|----------|----------|------------------------------------------------|
| New Vehicle | LLLL11 | BCDF12 | Vehicles, 4+ wheels (LLLL·nn) |
| New Motorcycle | LLL11 | BJH61 | Motorcycles, since 2014 (LLL·nn) |
| Old Vehicle | LL1111 | AR1240 | Pre-2007 standard (LL·nnnn), series-checked |
| Special | LL1111 | CD1202 | Diplomatic, consular, police, etc. |
| Police | L1111 | Z1234 | Carabineros de Chile |
| Ambulance | A1111 | A6709 | Ambulance plates |
Letter sets differ by format. Vehicle series omit vowels and
M,N,Q; motorcycle series omit only vowels, so plates likeMMN01orZBQ31are valid motorcycles. The old format has no letter rule — it has a published table of 582 issued series, which this library checks against.Trailers (remolques) share the motorcycle format exactly and cannot be told apart from the plate string alone; distinguishing them needs registration data this library does not have.
plateType()reports them asnew_motorcycle_plate.Heads up — 2025 format change. A 2025 decree (D.O. 16-01-2025) moves vehicles to 5 letters + 1 digit and motorcycles to 4 letters + 1 digit, rolling out gradually (~2027 for motos, ~2029 for cars), plus a green plate for EVs/hybrids. These new formats are not yet validated by this library and will be added as they enter circulation.
🚀 Installation
npm install chilean-plate-validatorRequires Node 18 or newer.
🛠 Usage
Using functions
import { normalize, plateValid, plateType, specialPlateInfo } from 'chilean-plate-validator'
// Strict validation — input must be pre-normalized
plateValid('BCDF12') // true
plateValid('BC-DF12') // false
// Normalize first if the input may contain separators
plateValid(normalize('BC-DF12')) // true
// Get plate category
plateType('BCDF12') // 'new_vehicle_plate'
plateType('AA1234') // 'old_plate'
plateType('Z1234') // 'police'
plateType('GG1234') // 'invalid' — 'GG' was never issued as a series
// Get special plate metadata (old format only)
specialPlateInfo('CD1234') // { prefix: 'CD', label: 'Cuerpo diplomático', authority: 'governmental', ... }
specialPlateInfo('AA1234') // nullNon-string input is treated as "not a plate" rather than raising, since a dropped
OCR read arrives as null more often than as '':
normalize(null) // ''
plateValid(undefined) // false
plateType(null) // 'invalid'The old-format series table
The old LL·nnnn format is the only Chilean format whose valid letters are a
published list rather than a rule. The Registro Civil allocated 582 of the 676
possible two-letter combinations, and this library ships that table:
import { isOldSeries, OLD_SERIES, plateType } from 'chilean-plate-validator'
OLD_SERIES.size // 582
isOldSeries('AR') // true — an issued series
isOldSeries('GG') // false — never allocated
isOldSeries('PR') // true — reserved prefix (patente provisoria), outside the series table
plateType('AR1240') // 'old_plate'
plateType('GG1234') // 'invalid'This also sharpens regional detection: LL·nnnn is shared with Perú, so a
prefix that Chile never issued now resolves to Perú alone.
Fuzzy validation (OCR input)
When the plate string comes from an image recognition pipeline, fuzzy mode corrects visually confusable characters before validating:
import { plateValid, fuzzyPlateValid, fuzzyCorrect } from 'chilean-plate-validator'
// Strict mode — fails on OCR errors
plateValid('8CDF12') // false
// Option 1 — config flag
plateValid('8CDF12', { fuzzy: true }) // true — '8' corrected to 'B'
// Option 2 — shorthand
fuzzyPlateValid('8CDF12') // true
// Get the corrected plate(s)
fuzzyCorrect('8CDF12') // ['BCDF12']
fuzzyCorrect('6CDF12') // ['GCDF12'] — 6 read as G
fuzzyCorrect('BCDF1Z') // ['BCDF12'] — Z read as 2
fuzzyCorrect('BCDF12') // ['BCDF12'] — already valid
fuzzyCorrect('AEIOU9') // []fuzzyCorrect returns every valid plate the input could have been, so
callers must handle more than one result. A character can be confusable with
several others, and a 6-character string can satisfy both the old and the new
format:
fuzzyCorrect('BCDGT2') // ['BCDG72', 'BC0672'] — new-format and old-format readingsWhich corrections pay off depends on the format's letter set. 0→O and 1→I
can only ever produce a valid old-format plate, because the new formats
exclude both letters — 0 is corrected to D for new-format candidates.
Using the PlateType object
Avoid hardcoding type strings — use PlateType for comparisons:
import { plateType, PlateType } from 'chilean-plate-validator'
if (plateType('BCDF12') === PlateType.NewVehicle) {
// ...
}Using the CLPlate class
The class normalizes input automatically and computes its results once:
import { CLPlate } from 'chilean-plate-validator'
const plate = new CLPlate('bc-df12')
plate.clean // 'BCDF12'
plate.isValid // true
plate.type // 'new_vehicle_plate'
plate.formatted // 'BCDF-12'
plate.specialInfo // null
const special = new CLPlate('CD-1234')
special.isValid // true
special.type // 'old_plate'
special.formatted // 'CD-1234'
special.specialInfo // { prefix: 'CD', label: 'Cuerpo diplomático', authority: 'governmental', ... }For OCR input, use the fuzzy getters on the instance or the static helpers:
// Instance — fuzzy validity is a superset of strict validity
const plate = new CLPlate('8CDF12')
plate.isValid // false — strict validation fails
plate.isFuzzyValid // true — recoverable via OCR correction
plate.fuzzyCorrections // ['BCDF12']
// Already-valid plates are also fuzzy-valid and correct to themselves
new CLPlate('BCDF12').isFuzzyValid // true
new CLPlate('BCDF12').fuzzyCorrections // ['BCDF12']
// Unrecoverable input
new CLPlate('AEIOU9').isFuzzyValid // false
new CLPlate('AEIOU9').fuzzyCorrections // []// Static helpers — no instantiation needed
CLPlate.normalize('BC-DF 12') // 'BCDF12'
CLPlate.fuzzyCorrect('8CDF12') // ['BCDF12']
CLPlate.isValidFuzzy('8CDF12') // true// toString() — coercion-friendly
String(new CLPlate('BCDF12')) // 'BCDF-12'
`Plate: ${new CLPlate('AA1234')}` // 'Plate: AA-1234'🌎 Regional detection (optional)
A separate entry point answers a different question: is this string a plate from Chile or a neighbouring country? Covers Chile, Argentina, Bolivia, Brasil, Paraguay, Perú and Uruguay.
import { detectOrigin, normalize } from 'chilean-plate-validator/regional';
detectOrigin('AB123CD') // ['AR'] — Mercosur car, unambiguous
detectOrigin('ABC1D23') // ['BR'] — Mercosur
detectOrigin('ABCD123') // ['PY'] — Mercosur car
detectOrigin('1234ABC') // ['BO']
detectOrigin('A1B234') // ['PE'] — taxi / urban bus
detectOrigin('BCDF12') // ['CL']
detectOrigin('AB1234') // ['CL', 'PE'] — shared shape, Chile first
detectOrigin('GG1234') // ['PE'] — 'GG' is not a Chilean series
detectOrigin('ABC1234') // ['BR', 'UY']
detectOrigin('HOLA') // [] — not a plate in any supported country
detectOrigin(normalize('ab-123-cd')) // ['AR']An empty array means the string matches no known format. Several codes mean the shape is used by more than one country and cannot be resolved from the string alone; results are ranked with Chile first, so taking [0] biases toward the local reading.
detectOriginDetailed returns the matching table entries instead of bare codes,
so you can show why a string was attributed:
import { detectOriginDetailed } from 'chilean-plate-validator/regional';
detectOriginDetailed('ABC1234').map((m) => `${m.country}: ${m.label}`)
// ['BR: Patente brasileña de formato antiguo (1990-2018)',
// 'UY: Patente uruguaya de automóvil o motocicleta (desde 2001)']This detects shapes, not valid plates. A match means the string looks like a plate from that country — no foreign registry, series or check digit is verified. Only Chilean plates are truly validated. This limit is deliberate: tracking seven legislations is unmaintainable, while shapes change roughly once a decade.
Every Mercosur-era format is unambiguous; the overlaps are all between older national formats.
| Shape | Countries |
|---|---|
| LLLLDD, LLLDD, LDDDD | 🇨🇱 Chile only |
| LLDDDLL, LDDDLLL | 🇦🇷 Argentina (Mercosur, car / motorcycle) |
| LLLDLDD | 🇧🇷 Brasil (Mercosur) |
| LLLLDDD, DDDLLLL | 🇵🇾 Paraguay (Mercosur, car / motorcycle) |
| DDDDLLL | 🇧🇴 Bolivia |
| LDLDDD | 🇵🇪 Perú (taxi / bus) |
| LLDDDD | 🇨🇱 Chile (old, series-checked) · 🇵🇪 Perú |
| LLLDDDD | 🇧🇷 Brasil (1990–2018) · 🇺🇾 Uruguay |
| LLLDDD | 🇦🇷 Argentina (1995–2016) · 🇵🇪 Perú · 🇵🇾 Paraguay (2000–2019) |
| DDDLLL | 🇦🇷 Argentina (motorcycle) · 🇧🇴 Bolivia (old) · 🇵🇾 Paraguay (motorcycle) |
Uruguay's
LLLDDDDis a strict subset of Brazil's: Uruguay usesA–Sin the first position for its 19 departments, and Brazil's range covers that band. So a Uruguayan-looking plate always reports['BR', 'UY'], and only a first letter pastSresolves to Brazil alone.
OCR note: don't pass
fuzzyCorrect()output todetectOrigin(). Those corrections assume Chilean character positions and will push foreign plates toward Chilean shapes. Detect on the raw normalized read instead — a clean foreign match is a good reason to skip fuzzy correction altogether.
🧪 Development
nvm use # Node version from .nvmrc
npm ci
npm run verify # lint + typecheck + tests + build + smoke test| Script | What it does |
|---|---|
| npm test | Jest suite, including every documented example |
| npm run test:coverage | Same, with a coverage report |
| npm run lint | ESLint over src and tests |
| npm run typecheck | tsc over src and tests |
| npm run build | Emits dist/ via tsdown (ESM + CJS + types) |
| npm run smoke | Resolves and exercises the built package, as a consumer would |
| npm run verify | All of the above, in order. Also runs on prepublishOnly |
Every @example in the JSDoc and every fenced block in this README is executed
by tests/examples.test.ts and compared against real output, so a documented
example cannot drift from behaviour.
📚 Legal basis & documentation
This module follows the official specifications from the Servicio de Registro Civil e Identificación de Chile.
- Official manual (registrocivil.cl)
- Local copy in this repository — the source of the 582-series table
📄 License
This project is licensed under the MIT License.
👤 Author
Gabriel Galilea
- GitHub: @gabo2151
- LinkedIn: Gabriel Galilea
