The UK Companies House company number (CRN) as a code

Built in 0.2.0 as a pattern system: tools/load_crn.sql defines uk_crn; the findings below are what it was designed from. Checked on all 13,549,728 numbers in the Companies House Bulk Downloads: every one casts to code, text and code agree on the way back, the codes sort as the text does, the prefix counts match the table’s, code_first_with_prefix('SC','uk_crn').. code_last_with_prefix finds the 659,559 SC numbers by an index-only scan, and code_from_padded('1234567','uk_crn') restores a number a spreadsheet stripped. The universe it defines is 407,291,360 codes. Parts: prefix and number; the older society numbers have legacy_prefix and legacy_number, because a part name can be used once in a pattern.

Exploration

Run on 2026-10-11 against the Companies House Bulk Downloads (the 2026-09 monthly file, the dissolved-company search data, and the CRNs seen in the update feed): 13,549,728 distinct CRNs, 5.69 M of them in the current monthly file.

What the data shows

All 13.5 M are 8 characters, upper case, no spaces. By shape:

shape number what it is
99999999 12,393,857 England & Wales: 8 digits, zero padded (1.5 M of the live ones start with 0); highest seen 17,429,089
AA999999 1,149,885 a 2-character prefix and 6 digits, 35 prefixes: SC (660 k), NI, OC, CE, SL, OE, LP, FC, CS, SO, RS, NC, IP, NL, RC, AC, IC, NF, BR, GE …
A9999999 153 R0 + 6 digits (the prefix contains a digit)
the legacy society numbers 5,833 (0.043 %) IP, SP, RS and a few SL: IP00002C, SP1000RS, RS0001FI, SP001CUS, and four one-offs (IPIP2960, IPIP316C, SP3COLLS, SP01BENS) – a number with a one- to three-letter suffix

A CRN is not a classification code: the set is open (it grows daily, and the E&W numbers are allocated in sequence), has no list to be checked against, and has no hierarchy. Its only structure is the prefix, which says the kind of body and the jurisdiction.

What a pattern gives (tools/probe_pattern.c)

The right model is the engine’s other kind of definition, a pattern: it defines the universe of syntactically possible CRNs, and a code’s value is its rank in that universe, so adding companies changes nothing – no revisions are needed.

  • \d{8}|(35 prefixes)\d{6}: 135,000,000 codes (27.0 bits), 28 states. Rejects exactly the 5,833 legacy numbers.
  • the same plus the legacy forms ((IP|RS|SP|SL)\d{5}[A-Z], (IP|RS|SP)\d{4}[A-Z]{2}, (IP|RS|SP)\d{3}[A-Z]{3}, SP\d{2}[A-Z]{4}, SP\d[A-Z]{5}, IPIP\d{4}, IPIP\d{3}[A-Z]): 382,955,360 codes (28.5 bits), 58 states. All 13,549,728 real CRNs parse, every one renders back to the same text, ranks come out in byte order, about 150 ns per parse plus render.

What it does not give

  • Existence. The universe is 28x the number of CRNs ever seen (E&W: 100 M possible, 17.4 M issued). The type would catch a typo in the prefix, a wrong length, lower case, a stray letter; it would not catch a number nobody was issued. That is a join to the Companies House table, as now.
  • A smaller column. A code is 8 bytes; the text is 9. (28.5 bits would fit in an int4, but that is another type.)
  • A tight range. Capping the E&W digits at what has been issued would catch more typos, but the cap grows every year, so every extension is a new revision and values written earlier would need code_current(). Not capping is the stable choice.

Practical points

  • Spreadsheets drop the leading zeros of E&W numbers (01234567 becomes 1234567). A padded input form – digits only, shorter than 8, padded – belongs in a helper (code_from_padded), not in the type: a short digit string is not a CRN, and accepting it silently is the kind of leniency that hides a bad load.
  • The prefix is the only part: code_part(c, 'prefix') needs the named-part support of the engine (cs_parts) in the SQL layer, and an E&W number has no prefix, so the part is optional.
  • Pattern systems (0.2.0): the revision stores the regular expression (not millions of rows); the system is flat and may name parts.
  • The same shape fits other identifiers with a syntax and no list: charity numbers, the GSS codes' format, … but not the ones with check digits (out of scope) or ones that need the list (those are the existing kind).