All posts

Corgi v3: Binary Indexes and What a Tiny LLM Learned About VINs

Sam Sullivan8 min read
Corgi v3: Binary Indexes and What a Tiny LLM Learned About VINs

This started with a GitHub issue and ended with us training a neural network that reverse-engineered the VIN specification from scratch.

A simple question

@wonderooo opened issue #21 on corgi: "Are you planning to add the option to decode a batch of VIN numbers in a single DB query?"

Simple question. The answer was a complete rewrite.

Corgi v2 used SQLite. For single VIN lookups, it was fine—30ms, no network required, runs offline. But batch decoding exposed a fundamental problem with the architecture.

Here's what happens when you decode a Ford F-150 VIN: the decoder queries 1000+ pattern rows, then joins each match against 20+ attribute tables. Engine specs. Transmission. Body style. Drive type. Plant location. Safety equipment. Each attribute lives in its own lookup table because that's how NHTSA structured the data in 1981.

For one VIN, that's annoying but manageable. For 1000 VINs, you're making roughly 4000 database queries. On serverless platforms like Cloudflare D1 or Turso, you hit read throttling around 200-500k operations. The "fast offline decoder" becomes slower than just calling the NHTSA API.

I'd always had a hunch that binary indexes would work better—precompute everything at build time, ship a blob, do O(log n) lookups at runtime. But I hadn't proven it would actually be faster for our access patterns.

wonderooo did. He built corgi-rs, a Rust implementation using FST (finite-state transducers) with rkyv serialization. The key insight: instead of 4000 queries to decode 1000 VINs, you could do it in roughly 4 index lookups. Not by optimizing SQL. By removing SQL entirely.

The v3 rewrite

We rebuilt corgi from scratch around binary indexes.

The format is simple: a 32-byte header, an offset table, sorted keys, and MessagePack-encoded values. No query planning, no SQL parsing, no B-tree traversal. Just binary search on sorted strings.

text
┌─────────────────────────────────────────┐
│ Header (32 bytes)                       │
│   magic: "CORG"                         │
│   version, keyCount, offsets...         │
├─────────────────────────────────────────┤
│ Offset Table (16 bytes per entry)       │
├─────────────────────────────────────────┤
│ Keys (sorted UTF-8, binary searchable)  │
├─────────────────────────────────────────┤
│ Values (MessagePack encoded)            │
└─────────────────────────────────────────┘

The results were better than expected:

Metricv2 (SQLite)v3 (Binary)
Cold start~200ms23ms
Single decode~30ms0.3ms
Batch (1000 VINs)Throttled300ms
Index size (raw)64 MB64 MB
npm package (gzip)21 MB6.5 MB
CDN delivery (brotli)2.8 MB

Corgi v2 vs v3 Benchmark Comparison

And because there's no native SQLite binding, it runs everywhere: Node, browsers, Cloudflare Workers, Deno, Bun. Same code, same indexes.

typescript
import { createDecoder } from '@cardog/corgi';

const decoder = await createDecoder();
const result = await decoder.decode('5YJ3E1EA1PF123456');
// { make: 'Tesla', model: 'Model 3', year: 2023, bodyType: 'sedan', ... }

Then we found the real problem

With v3 working, we started validating decode accuracy against our listing database. 1.17 million vehicles. 89,000 unique VIN patterns. 312,000 known variants across US, Canada, and EU markets.

The decoder was 93.6% accurate on Tier 1 attributes—make, model, year, body style, engine, drivetrain. The stuff that's actually encoded in the VIN.

But when we tried to resolve VINs to specific trims, everything fell apart.

ISO 3779 standardized the VIN format in 1981, but it only standardized the structure—not the semantics. Positions 4-8 are the "Vehicle Descriptor Section" but what they describe is up to each manufacturer. Some encode trim level. Most don't.

We ran the numbers across makes:

MakeEncodes Trim?
Buick, GMCUsually
Chevrolet, FordSometimes
HondaRarely
Toyota, Mazda, HyundaiAlmost never

VIN Tier 1 vs Tier 2 Data Availability

This is the tier problem:

  • Tier 1: Make, model, year, body, engine, drivetrain—encoded in the VIN, decodable offline
  • Tier 2: Trim level, packages, individual options—NOT in the VIN, requires external data

Corgi solves Tier 1. Tier 2 needs something else entirely.

Teaching a neural network to read VINs

While debugging decode accuracy, we started experimenting with a different approach. What if instead of hand-coded pattern matching, we trained a model to predict vehicle attributes from VIN characters?

We built VORTEX (Vehicle Ontology Resolution Through EXplanatory Compression)—a 6-layer decoder-only transformer with 6.6M parameters and 256-dimensional embeddings. Tiny by modern standards. We trained it on 50,000 VIN→vehicle pairs from our database.

Training took 8 minutes on a laptop.

VORTEX Training Loss

The accuracy was good—100% on make, 99% on model, 90% on trim. But the interesting part wasn't the accuracy. It was what the model learned.

VORTEX Trim Prediction Accuracy by Make

When we visualized the embeddings, vehicles clustered by category. All the pickup trucks grouped together. SUVs formed their own cluster. Sedans, another. Within each cluster, similar vehicles had near-identical embeddings—0.99 cosine similarity between a Toyota Camry and Honda Accord.

Vehicle Embedding Space

More surprising: the model had learned the VIN encoding scheme without being told anything about it.

Position 10 in a VIN encodes model year using a letter/number cycle: N=2022, P=2023, R=2024, S=2025. We never told the model this. But when we probed the attention patterns, position 10 dominated year predictions. The model figured out the spec from data alone.

Same with WMI prefixes. The model learned that VINs starting with 1FT are Fords, 5YJ are Teslas, WBA are BMWs. Not because we provided a WMI lookup table—we didn't. It learned the mapping from co-occurrence patterns in training data.

A 25MB file of weights that reverse-engineered ISO 3779.

VORTEX Model Performance

We're beginning to integrate VORTEX for Tier 2 resolution. The approach: contrastive learning on listing data, using price signals and crowd consensus to disambiguate trim levels. If 95% of listings with a given VIN pattern are priced like an XLT and described as an XLT, it's probably an XLT. Early results are promising.

We'll do a deeper dive on VORTEX—the architecture, training process, and what the attention patterns reveal—in an upcoming post.

The vehicle data mess

Working on this surfaced just how fragmented vehicle data is across the industry.

US data providers catalog vehicles with tens of thousands of unique feature keys—as booleans. "12.3\" infotainment display size": true. The value is embedded in the key name. There's no schema, no hierarchy, no way to query "all vehicles with displays larger than 10 inches" without parsing thousands of string keys.

Canadian providers use nested category structures. Better organized, but incompatible with US schemas.

European sources have detailed mechanical specs but almost no feature data. Great if you want bore and stroke measurements. Useless if you want to know whether it has CarPlay.

And trim names are chaos across regions. The same car:

  • US: "Premium Plus"
  • Canada: "Progressiv"
  • Germany: "45 TFSI (265 Hp) Mild Hybrid quattro S tronic"

The US name describes feature level. The German name describes powertrain. Canada tried to split the difference.

VIS: What we're building

There's no universal schema for vehicle data. ISO 3779 defines VIN structure but not what the decoded data should look like. Schema.org has a Vehicle type but it's missing critical fields for modern vehicles—no powertrainType for EVs/hybrids, no battery specification, no ADAS features, no connectivity.

We're building VIS (Vehicle Identity Standard) to fix this. It extends Schema.org with a vis: namespace for what's missing:

  • powertrainType: ice, bev, hev, phev, mhev, fcev
  • battery: capacity, chemistry, range
  • electricMotor: power, position, count
  • adasFeatures: adaptiveCruiseControl, automaticEmergencyBraking, laneKeepAssist
  • connectivityFeatures: appleCarPlay, androidAuto, wirelessCharging

The goal: any VIN decoder, any data source, same output format. Normalize US, Canadian, and EU data into one schema. Make vehicle data interoperable across the industry.

Corgi v3 outputs follow VIS structure, so downstream systems don't need to know whether the data came from NHTSA patterns or VORTEX inference.

We'll publish the full VIS spec and write more about the schema design decisions in a future post.

What's still to be done

  • Chinese domestic market. Tesla Shanghai VINs start with LRW. BYD, NIO, XPeng have their own WMIs. We're actively working on patterns for these vehicles. Our community pipeline is published here and is welcome to contributions.
  • Pre-1981 vehicles. Before ISO 3779, every manufacturer used their own VIN format. Different lengths, different encodings, no check digit. We don't support them and probably never will.
  • Regional trim mapping. "Long Range" and "长续航全轮驱动版" are the same Tesla Model Y variant. We don't have canonical mappings between regional trim names yet. VORTEX might help here—embeddings for equivalent trims should cluster—but it's unsolved.

Credits

This wouldn't exist without @wonderooo. The corgi-rs implementation proved the architecture, and the conversation in issue #21 pushed us to actually build it. Open source at its best.


Try it: npm install @cardog/corgi

Source: github.com/cardog-ai/corgi

Docs: docs.cardog.app/corgi