Home > Uncategorized > Cracking the Code: Estimating a Car’s Age from Its Argentine Licence Plate

Cracking the Code: Estimating a Car’s Age from Its Argentine Licence Plate

Technical Deep-dive · Vehicle Data

How sequential plate issuance, a little combinatorics, and 200,000 training records let you estimate a registration year from seven characters.By Infinite Loop Development  ·  March 2026

Try it live

The techniques described in this post are implemented in ar.matriculaapi.com — an API that returns full vehicle details for any Argentine licence plate, including an estimated registration year for plates where the exact date is unknown.

Every Argentine licence plate encodes its own approximate birth year. You just have to know how to read it.

Argentina has issued plates in two distinct sequential formats over the past three decades. Because they are allocated in strict national order, a plate’s position in that sequence maps — with surprising precision — to the year the vehicle was registered. This post explains the technique: the encoding, the boundary estimation, and the confidence model.


Two Formats, One Principle

Argentina uses two plate formats, each covering a different era.

OZY040

Pre-Mercosur · ABC123≈ 1990 – 2016

AC875MD

Mercosur · AB123CD2016 – present

The Pre-Mercosur format (ABC123) consists of three letters followed by three digits. It was Argentina’s standard from the early 1990s through approximately 2016, when the country transitioned to the regional Mercosur standard.

The Mercosur format (AB123CD) uses two letters, three digits, then two more letters. It began with AA000AA and has been incrementing steadily ever since, shared across the Mercosur bloc — which is why plates from Brazil, Uruguay, and Paraguay share the same structure.

The critical insight is that both formats were issued sequentially at a national level. A plate allocated in 2019 will always have a higher sequence number than one from 2018. This makes year estimation a matter of finding where a given plate falls in the sequence.


Encoding a Plate to a Single Integer

To compare plates across their sequence, we convert each plate to a single integer using mixed-radix encoding — the same idea as a number system that switches base partway through.

Mercosur encoding

The Mercosur plate AB123CD has four alphabetic components (each 0–25) and one numeric component (0–999). Treating letters as base-26 and the number as base-1000:

Python

def encode_mercosur(plate: str) -> int:
    """
    Encode an AB123CD Mercosur plate to a sequence integer.
    AA000AA = 0, AA000AB = 1, ... AZ999ZZ = 17,575,999, BA000AA = 17,576,000 ...
    """
    p = plate.upper().replace(" ", "").replace("-", "")
    assert len(p) == 7, "Mercosur plates are 7 characters"

    l1 = ord(p[0]) - ord('A')  # 0-25
    l2 = ord(p[1]) - ord('A')  # 0-25
    n  = int(p[2:5])             # 0-999
    l3 = ord(p[5]) - ord('A')  # 0-25
    l4 = ord(p[6]) - ord('A')  # 0-25

    return (l1 * 26 + l2) * 676_000 \
         + n              * 676     \
         + l3             * 26      \
         + l4

A few spot checks: AA000AA → 0. AA000AB → 1. AA001AA → 676. AB000AA → 676,000. The encoding is monotonically increasing — every plate that comes later in the alphabet maps to a strictly higher integer.

Pre-Mercosur encoding

The earlier ABC123 format encodes similarly, but with three leading letters instead of two:

Python

def encode_pre_mercosur(plate: str) -> int:
    """
    Encode an ABC123 pre-Mercosur plate to a sequence integer.
    AAA000 = 0, AAA001 = 1, ... ZZZ999 = 17,575,999
    """
    p = plate.upper().replace(" ", "").replace("-", "")
    assert len(p) == 6, "Pre-Mercosur plates are 6 characters"

    l1 = ord(p[0]) - ord('A')  # 0-25
    l2 = ord(p[1]) - ord('A')  # 0-25
    l3 = ord(p[2]) - ord('A')  # 0-25
    n  = int(p[3:])             # 0-999

    return (l1 * 676 + l2 * 26 + l3) * 1000 + n

Learning the Boundaries from Real Data

Encoding gives us integers. To turn those integers into years, we need to know which sequence ranges correspond to which years. This is where training data comes in.

Using over 200,000 Argentine plates with known registration years, we computed the mean sequence number per year. This gives a representative centroid for each year’s plate population:

The year boundary cut points are simply the midpoints between adjacent means. This produces clean, non-overlapping ranges — every sequence integer maps to exactly one year:

Seq rangeEstimated yearCut point derivation
< 671,6192016(294k + 1,048k) / 2
671,619 – 1,467,1702017(1,048k + 1,885k) / 2
1,467,170 – 2,207,1362018(1,885k + 2,528k) / 2
2,207,136 – 2,719,7202019(2,528k + 2,910k) / 2
2,719,720 – 3,088,3062020(2,910k + 3,265k) / 2
3,088,306 – 3,472,7692021(3,265k + 3,679k) / 2
3,472,769 – 3,888,5342022(3,679k + 4,097k) / 2
3,888,534 – 4,307,0052023(4,097k + 4,516k) / 2
4,307,005 – 4,773,8592024(4,516k + 5,030k) / 2
≥ 4,773,8592025(open upper bound)

The naive approach used min/max ranges per year — but these overlap badly. Using the mean and splitting at midpoints gives clean, unambiguous boundaries.


The Full Estimator in Python

Python

import re
from dataclasses import dataclass
from typing import Optional

# ── Mercosur year boundaries (midpoints between annual means) ──────────────
# Derived from 200k+ Argentine plates with known registration years.
# Each January, recalculate the mean for the new year and add one entry:
#   new_cut = (mean_prev_year + mean_new_year) / 2
# ──────────────────────────────────────────────────────────────────────────
MERCOSUR_CUTS = [
    (671_619,   2016),
    (1_467_170, 2017),
    (2_207_136, 2018),
    (2_719_720, 2019),
    (3_088_306, 2020),
    (3_472_769, 2021),
    (3_888_534, 2022),
    (4_307_005, 2023),
    (4_773_859, 2024),
    (float('inf'), 2025),
    # Add 2026 here in January 2027:
    # (cut_2025_2026, 2025), (float('inf'), 2026),
]

# Pre-Mercosur: two-letter prefix → dominant year
# Derived from same training dataset. Stable — no new plates since ~2016.
PRE_MERCOSUR_PREFIX = {
    # A block 1994-1996
    'AA': lambda n: 1994 if n < 500 else 1995,
    'AB': 1995, 'AC': 1995, 'AD': 1995, 'AE': 1995, 'AF': 1995,
    'AG': 1995, 'AH': 1995, 'AI': 1995, 'AJ': 1995, 'AK': 1995,
    'AL': 1995, 'AM': 1995, 'AN': 1995,
    'AO': lambda n: 1995 if n < 400 else 1996,
    'AP': 1996, 'AQ': 1996, 'AR': 1996, 'AS': 1996, 'AT': 1996,
    'AU': 1996, 'AV': 1996, 'AW': 1996, 'AX': 1996, 'AY': 1996, 'AZ': 1996,
    # B block 1996-1998
    'BA': 1996, 'BB': 1996,
    'BC': lambda n: 1996 if n < 300 else 1997,
    'BD': 1997, 'BE': 1997, 'BF': 1997, 'BG': 1997, 'BH': 1997,
    'BI': 1997, 'BJ': 1997, 'BK': 1997, 'BL': 1997, 'BM': 1997,
    'BN': 1997, 'BO': 1997,
    'BP': lambda n: 1997 if n < 500 else 1998,
    'BQ': 1998, 'BR': 1998, 'BS': 1998, 'BT': 1998, 'BU': 1998,
    'BV': 1998, 'BW': 1998, 'BX': 1998, 'BY': 1998, 'BZ': 1998,
    # C-P blocks follow same pattern; see full table at ar.matriculaapi.com
    # ... (abbreviated for readability)
}

PRE_MERCOSUR_RE = re.compile(r'^[A-Z]{3}\d{3}$')
MERCOSUR_RE     = re.compile(r'^[A-Z]{2}\d{3}[A-Z]{2}$')


@dataclass
class PlateEstimate:
    input_plate:    str
    format:         str         # 'MERCOSUR' | 'PRE-MERCOSUR' | 'MERCOSUR-IMPORT' | 'UNKNOWN'
    estimated_year: Optional[int]
    confidence:     str         # 'HIGH' | 'MEDIUM' | 'LOW'
    sequence_num:   Optional[int]
    notes:          Optional[str] = None


def estimate_year(plate: str) -> PlateEstimate:
    p = plate.upper().replace(" ", "").replace("-", "")

    # ── Mercosur AB123CD ─────────────────────────────────────────────────────
    if MERCOSUR_RE.match(p):
        seq = encode_mercosur(p)

        if seq < 7:
            return PlateEstimate(plate, 'MERCOSUR-IMPORT', None, 'LOW', seq,
                notes='Sequence predates Argentine rollout; likely import')

        year = next(y for cut, y in MERCOSUR_CUTS if seq < cut)

        # Confidence: MEDIUM if within 5% of the nearest boundary
        confidence = 'HIGH'
        cuts = [c for c, _ in MERCOSUR_CUTS[:-1]]
        gaps = [MERCOSUR_CUTS[i+1][0] - MERCOSUR_CUTS[i][0]
                for i in range(len(cuts))]
        for cut, gap in zip(cuts, gaps):
            if abs(seq - cut) < gap * 0.05:
                confidence = 'MEDIUM'
                break

        return PlateEstimate(plate, 'MERCOSUR', year, confidence, seq)

    # ── Pre-Mercosur ABC123 ───────────────────────────────────────────────────
    if PRE_MERCOSUR_RE.match(p):
        prefix = p[:2]
        num    = int(p[3:])
        entry  = PRE_MERCOSUR_PREFIX.get(prefix)

        if entry is None:
            return PlateEstimate(plate, 'PRE-MERCOSUR', None, 'LOW', None,
                notes=f'Prefix {prefix!r} not in training data')

        year = entry(num) if callable(entry) else entry

        confidence = ('LOW'    if prefix[0] in 'RSTUV'
                 else 'MEDIUM' if prefix <= 'DZ'
                 else 'HIGH')

        return PlateEstimate(plate, 'PRE-MERCOSUR', year, confidence,
                              encode_pre_mercosur(p))

    return PlateEstimate(plate, 'UNKNOWN', None, 'LOW', None,
        notes='Does not match any known Argentine plate format')


# ── Quick demo ────────────────────────────────────────────────────────────
for test in ['AC601QQ', 'AC875MD', 'OZY040', 'GDA123', 'AB 172UC']:
    r = estimate_year(test)
    print(f"{r.input_plate:10} → {r.estimated_year}  [{r.confidence}]  {r.format}")

Why Pre-Mercosur Is Trickier

You might expect pre-Mercosur plates to work the same way as Mercosur — encode to an integer, find the range. But the raw data tells a different story: the sequence number ranges for adjacent years overlap almost completely.

The reason is that Argentina’s provinces received independent plate allocations. Buenos Aires, Córdoba, and Mendoza were all issuing plates simultaneously from their own provincial ranges. A national sequence number alone therefore can’t pinpoint a year — the number space was being consumed by 23 provinces in parallel.

What does cluster cleanly by year is the two-letter prefix. The national allocation advanced through the alphabet over time, so GA–GT plates are overwhelmingly from 2007, HT–HZ from 2009, and so on. The training data confirms this: for the densest prefixes, over 90% of plates share a single dominant year.

At prefix boundaries — AOBCGTHS, and others — the numeric suffix provides a secondary signal. A plate in the GT prefix with a low number (GT100) likely precedes one with a high number (GT850) by several months, straddling a year boundary.


Confidence and Outliers

The estimator returns one of three confidence levels:

HIGH  The plate sits comfortably within a year band — more than 5% away from any boundary. For Mercosur plates from 2017 onwards (where training density is highest) this is the common case.

MEDIUM  The plate is within 5% of a year cut point, or belongs to a pre-Mercosur prefix with moderate training data. The estimate is most likely correct but the adjacent year is plausible — late registrations, delivery delays, and data entry lag all introduce real ambiguity near boundaries.

LOW  The prefix is sparse (old R/S/T/U/V plates, typically pre-1995) or the plate is flagged as a likely import. Imports arise when a Mercosur-format plate has a sequence number that predates Argentina’s 2016 rollout — these vehicles were most likely registered in Brazil, Uruguay, or Paraguay and subsequently imported.


Keeping It Fresh

The Mercosur boundaries need updating once a year. The process is three steps:

1. Collect a fresh batch of plates with known years.
2. Compute the mean sequence number for the new year’s cohort.
3. Set the new cut point as (mean_prev + mean_new) / 2 and append it to the table.

No existing cut points change — you’re only ever adding one new entry to the bottom of the list. The pre-Mercosur prefix table is stable and needs no maintenance, since no new plates in that format have been issued since approximately 2016.


Beyond Year Estimation

Year estimation is useful on its own — for insurance quoting, fleet valuation, or fraud detection — but it becomes much more powerful when combined with a full vehicle lookup.

The Argentine Matricula API takes any plate (pre-Mercosur or Mercosur) and returns the complete vehicle record: make, model, year, engine, and more. For records where the registration year is missing or uncertain, the sequence-based estimate described here fills the gap automatically, with a confidence flag so downstream consumers know how much to trust it.

Argentine vehicle lookup API

Try a plate lookup at ar.matriculaapi.com. The API is available for integration via vehicleapi.com and supports batch queries, JSON responses, and per-format confidence scoring.

© 2026 Infinite Loop Development Ltd  

Categories: Uncategorized
  1. No comments yet.
  1. No trackbacks yet.

Leave a comment