Acest articol nu a fost încă tradus în Română — citești originalul în English. Disponibil și în:Deutsch, English, Українська
How Our Person Generator Actually Works — And What It Still Gets Wrong
Most fake-name generators are one function: pick a name from a list, pick a city from another list, glue them together. Ours is not much more than that either — but the difference between "not much more" and "one function" is where all the interesting problems live.
This article documents the whole machine: what it takes into account, how it stays reproducible, and — the section that matters more — the list of things it demonstrably does not model. Everything below was measured against the shipped code and data files on 18 July 2026, not against the design documents.
The twelve inputs
A generated card is a pure function of (locale, seed). There is no database at runtime. Here is everything that feeds into it.
1. Country
64 locales, each with its own corpora for given names, surnames, streets, cities, postcodes, companies and phone numbers. "Locale" here is closer to country than to language: de_DE, de_AT and de_CH are three separate datasets, and so are fr_CH, de_CH and it_CH — same country, three languages, one shared phone numbering plan.
Address word order is per-locale, because a house number is not placed the same way twice:
| Pattern | Locales | Example |
|---|---|---|
{n} {s} | en_US, en_GB, fr_FR, vi_VN … | 192 Mill Street |
{s} {n} | default — de_DE, pl_PL, nl_NL … | Schillerstraße 84 |
{s}, {n} | uk_UA, ru_RU, es_ES, it_IT … | вул. Широка, 174 |
{s} nr. {n} | ro_RO, ro_MD | Strada Spitalului nr. 13 |
{s} {n}. | hu_HU | Honvéd utca 52. |
{s} No:{n} | tr_TR | Kale Sokak No:71 |
{s}{n}號 | zh_TW (号 for zh_CN) | 中央路124號 |
The earlier version of this code hard-coded {s}, {n} — the Ukrainian model — for all 64 locales. Every American address read as Mill Street, 192.
Full-name order is separate again. zh_CN, zh_TW, ja_JP and ko_KR put the surname first with no space (王達仁, not 達仁 王); hu_HU and vi_VN put it first with a space (Ferenczi Bódog). Everything else is First Last.
63 of 64 locales carry curated street files. ja_JP deliberately does not: a Japanese address is 丁目-番-号 (block–lot–building), not street-plus-number, and inventing a street layer for Japan would be inventing a system the country does not have.
2. Sex
Given names and surnames are drawn from sex-specific corpora, but the reason the corpora differ splits three ways:
| Group | Count | What is going on |
|---|---|---|
| Identical surnames | 49 | en_US, de_DE, ja_JP — surname does not vary by sex |
| Declined surnames | 12 | Παπαδόπουλος ♂ / Παπαδοπούλου ♀; Novák / Nováková |
| Separate surname sets | 3 | en_IN, bn_BD, ka_GE — different lists, not paired forms |
The third group is the one that breaks naive validators. Sikh naming gives Singh largely to men and Kaur to women; Bengali Begum and Khatun are female-only. These are not two spellings of one name, so the male and female files legitimately have different lengths — en_IN ships 836 male and 842 female surnames, bn_BD 400 and 404. A validator that demands "the two files must match line for line" silently blocks the top surname of a whole country from being added anywhere.
Greek belongs in the second group, and that was a correction: it was first listed as invariant, which is wrong — -ος → -ου, -ης → -η, and 545 of 648 Greek surname pairs actually change.
Maiden name is always drawn from the female corpus, because a maiden name is the mother's surname before marriage regardless of the cardholder's sex.
3. Year of birth → age cohort
For ten locales — de_AT, en_CA, en_GB, en_IE, en_NZ, en_US, es_ES, fr_FR, it_IT, no_NO — the given name is drawn from a corpus built for the decade the card's person was born in, not from the country's all-time list. Eight of the ten cover all seven decades; the Austrian series starts in 1984 and the Italian one in 1999, which is a defect of its own and has its own article. Decade boundaries follow INSEE's convention: [1941, 1951, 1961, 1971, 1981, 1991, 2001], so 1950 lands in the 1941 bucket and 1951 opens the next.
The effect on the shipped corpora, ranked by real birth counts:
| Locale | Born ~1951 (top 6) | Born ~2001 (top 6) |
|---|---|---|
no_NO | Jan, Per, Bjørn, Kjell, Svein, Terje | Jonas, Andreas, Mathias, Martin, Daniel, Sander |
fr_FR | Jean, Michel, Patrick, Alain, Philippe, Christian | Lucas, Enzo, Thomas, Théo, Hugo, Nathan |
es_ES | Antonio, José, Manuel, Francisco, Juan, José Luis | Alejandro, Daniel, Pablo, David, Adrián, Álvaro |
en_IE | John, Patrick, Michael, James, Paul, Thomas | Jack, Sean, Conor, Adam, James, Daniel |
en_NZ | John, Peter, David, Michael, Stephen, Robert | Jack, Joshua, Samuel, James, Benjamin, Daniel |
en_US | Michael, James, Robert, David, John, William | Jacob, Michael, Joshua, Matthew, Ethan, Andrew |
en_CA | Robert, David, John, Michael, James, Richard | Ethan, Jacob, Matthew, Joshua, Liam, Ryan |
Norwegian and French turn over completely; American barely moves at the top, which is itself the finding — Anglophone boys' names churn slower. The mechanism, and the registry data behind it, is the subject of Your Name Is a Birth Certificate.
A dozen actual en_US male cards, drawn at the two extremes, look like this:
- born 1951: David, John, Julian, Jessie, Allen, Dan, Willie, Keith, Gary, Dennis, Rodger, Rick
- born 2004: Kevin, Caleb, Andrew, Jajuan, Carson, Jacob, Samson, Baylor, Talon, Benjamin, Daniel, Reece
Note that these are draws, not the top of the list — Rodger and Jajuan come out of the tail. That is the point of weighting: the head dominates without the tail disappearing.
4. Frequency
Names are weighted, not uniform. Until 18 July 2026 the weight was expressed by repeating a line in the corpus — weight 7 meant seven identical lines — which capped resolution at 255 and made the tail heavier the more complete the data got. Measured on the Taiwanese registry: at full depth (2,707 surnames) the top ten drew 24.9% where the real figure is 52.8%, an error of −27.9 pp that grew with every rare surname added. The old format punished completeness.
The corpus now holds unique values only, with weights in a parallel .wgt file — cumulative uint32, one per record, resolved by binary search. There is no ceiling, the cache is smaller, and the weight can be a literal count of bearers.
Twelve surname corpora now carry real registry counts rather than calibrated ranks, re-measured on 22 July 2026:
| Locale | Records | Sum of weights | Top surname | Top-10 share |
|---|---|---|---|---|
en_US | 2,064 | 129,931,469 | Smith — 2,369,644 | 10.80% |
es_ES | 2,522 | 34,026,331 | García — 1,446,937 | 24.94% |
en_GB | 1,517 | 27,125,558 | Smith — 652,563 | 12.05% |
zh_TW | 2,707 | 23,367,536 | 陳 — 2,618,994 | 52.81% |
fr_FR | 1,731 | 20,114,307 | Martin — 250,013 | 6.24% |
pl_PL | 28,959 | 16,682,538 | Nowak — 98,387 | 3.19% |
uk_UA | 2,562 | 9,752,950 | Мельник — 107,878 | 8.42% |
sv_SE | 2,445 | 5,594,332 | Andersson — 216,488 | 23.28% |
da_DK | 717 | 2,984,886 | Nielsen — 222,355 | 46.27% |
no_NO | 3,780 | 2,854,195 | Hansen — 47,879 | 11.72% |
vi_VN | 298 | 99,441 | Nguyễn — 30,492 | 69.58% |
en_IE | 908 | 80,922 | Murphy — 1,717 | 13.21% |
The spread is the whole argument for real counts: Poland's top ten reaches 3.19% and Vietnam's reaches 69.58%. Any generator that calibrates every country to the same concentration band erases a twenty-fold difference. The methodology for checking this — and the traps in checking it — is in How to Audit a Name-Frequency Dataset.
Given names are further behind: only eight locales run on real birth counts (en_US, en_GB, fr_FR, es_ES, sv_SE, no_NO, en_IE, en_NZ). Ukrainian surnames are on registry counts while Ukrainian given names are not — the locale is half-migrated, and saying "uk_UA has real weights" without that qualifier would be false.
5. City ↔ postcode as one pair
City and postcode are not two independent draws. Each locale ships locality/<locale>.tsv with rows of City⇥Region⇥Postcode, plus an optional fourth population column added on 18 July 2026. One row is drawn, and the city and postcode come out of it together, so they always agree. Region is carried for weighting and validation but is not printed on the card.
6. City size
As of 22 July 2026, 44 of 64 locales weight city selection by population. The difference is visible in 400 draws:
| Locale | Distinct cities hit | Top of the tally |
|---|---|---|
uk_UA (weighted) | 51 | Київ 67, Харків 38, Донецьк 21, Одеса 19 |
es_AR (unweighted) | 60 | Salta 13, Cipolletti 12, Rafaela 12, Concepción del Uruguay 11 |
In the Argentine column every one of the 60 cities came up, and Buenos Aires — three million people in the city proper — is drawn no more often than Rafaela, which has about 100,000. That is what uniform selection means, and it is invisible on any single card. The full treatment is in Why Address Generators Produce Implausible Cities.
7. Determinism — and why every draw is exactly one draw
The URL /<country>/<id> must return the same person forever. That is a product requirement, not a technical nicety: a permalink to a card is worthless if the card drifts.
The seed is derived by hashing the installation's secret salt together with the id, and the result initialises the PRNG. Everything downstream is drawn off that one stream. The exact hash, its truncation and the secret itself are deliberately not published here — they are the only part of this design that has to stay private, and the reason is the next section.
The subtle part is not the seeding. It is this pattern:
function pick(string $dataset): ?string
{
$h = open($dataset);
$keep = mt_rand(0, PHP_INT_MAX); // exactly one draw, in BOTH branches
$v = ($h !== null && $h['total'] >= 1) ? read_at($h, index_for($h, $keep)) : null;
mt_srand($keep); // stream returns to the same point in BOTH branches
return $v;
}
Every selection function spends exactly one mt_rand() and then re-seeds the stream to $keep. After the call the RNG sits at an identical point whether the dataset existed, was missing, or fell through to a fallback that burned an unknown number of draws.
Without that invariant, adding a single dataset shifts the stream for every field after it. Build one new corpus and every card on the site changes name, address, phone, password and birthday. Worse, we hit the live version of this: a worker that once saw a dataset missing cached the negative result and stayed on the Faker fallback for the rest of its life, so the same URL served different people from different PHP-FPM workers at the same moment. The fix was two parts — never cache a negative lookup, and make both branches cost one draw.
The same discipline covers the cohort lookup. A "first of these candidates" helper — try the decade-specific corpus, fall back to the general one — tries candidates in order but spends one draw total; a naive pick($a) ?? pick($b) ?? faker() would spend two or three and make the presence of cohort data reshuffle the entire card.
8. The card's creation year is encoded in its id
The id is not opaque. Some of its leading bytes carry structure rather than entropy — among them the archive version, the age range requested from the slider, a forced-sex flag, and the year the card was created. The exact offsets are omitted for the reason given in §7; what matters for this article is that the creation year is stored, not where.
Birth year is computed as card_year − age, not current_year − age. Without the stored year, every card would silently change its date of birth each 1 January: the age is stable from the seed, so the birth year would slide forward with the calendar. This byte was added on 16 July 2026, and ids issued before that carry a random value there — which is why it is clamped to [1990, current year]. Unclamped, a byte of 0x93 read as year 2147 and produced a card born in 2103.
9. The installation salt
The generator is deployed with one secret salt. Without it, /<country>/<id> would be a pure function of the id alone — anyone running the same code over the same corpora would hand back literally the same person for the same URL. The salt enters both the identity seed and the random-country hash, so it moves the whole card, not one field of it. Substituting a different salt and re-drawing the same id shows how total the effect is:
| Salt value (illustrative) | Same id, en_US, ages 30–60 | |
|---|---|---|
| `salt-a\ | v1` | Bryan Foster · 192 Mill Street · San Diego 92101 · 1994-11-24 |
| `salt-b\ | v1` | Michael Jones · 199 Forest Avenue · Fresno 93701 · 1989-02-01 |
| `salt-c\ | v1` | Jillian Thompson · 67 Lincoln Avenue · Oklahoma City 73101 · 1987-03-27 |
Not one field survives the change: name, street, city, postcode and date of birth are all different. The salts above are placeholders — the real value is not published, and that is the whole point of the mechanism. The |v1 suffix is the escape hatch: bumping it regenerates every card in the base. It is a one-shot resource, which is another reason not to spend it by publishing the current value.
10. Phone numbers
Phones come from a table of 963 real mobile prefixes across 65 locale entries, emitted as E.164 (+, country code, national number, no separators). Faker was dropped for this field for two measured reasons: within a single locale its format wandered (el_GR produced both 698 7681 770 and 6969005371), and several providers randomised the country code itself — es_AR emitted 35 distinct country codes and it_IT emitted 99. Every Italian card was carrying a phone number from a country picked at random.
Mobile ranges are used rather than landline: they are more current, and they are not tied to a specific city, which avoids claiming a precision (area code agreeing with city) that nothing else in the card supports.
11. Eircode
An Irish Eircode is a 3-character routing key plus a 4-character identifier for one building. Copying a code straight from the locality file would give every card in a town an identical Eircode — a defect any Irish reader spots immediately.
So the routing key is kept from the locality row (it is real and agrees with the town) and the four trailing characters are drawn from the Eircode alphabet, ACDEFHKNPRTVWXY0123456789 — 25 symbols, excluding B, G, I, J, L, M, O, Q, S, U, Z, which Eircode omits on purpose to prevent confusion between characters. Five consecutive cards:
Kildare R51 X775
Tullamore R35 5RT8
Drogheda A92 HNK0
Sligo F91 XAPK
Listowel V31 29FX
Those four extra draws happen only for en_IE; every other locale walks past the branch, so no other locale's cards shift.
12. Mail domain
The mailbox domain comes from a versioned, append-only archive of frozen snapshots, and the snapshot number is the first two hex digits of the id. A new card gets current; an old card reads its version out of its own id and gets that frozen snapshot forever. The live list of working domains changes constantly; permalinks do not.
What the generator does not do
Every item below is a real, reproducible limitation. None of them is theoretical.
Names do not vary by region within a country
Bavaria and Hamburg name children differently; so do Catalonia and Andalusia. We model one name distribution per country. A card from Munich and a card from Kiel draw from the identical corpus. Streets have the same flatness: there is one street file per country, so nothing distinguishes a Bavarian street list from a Hanseatic one.
Surnames do not vary by age
Cohort data exists for given names only. Migration-origin surnames have a pronounced age structure — a German surname corpus of 20-year-olds is not the corpus of 80-year-olds — and we flatten it completely. A 1941-born German card is as likely to draw Öztürk as a 2001-born one.
Given name and surname are not coordinated
This is the most visible defect on individual cards. The two fields are independent draws from independent corpora, so the layers cross. In de_DE, Turkish-layer given names carry 0.69% of the given-name weight and Turkish-layer surnames 0.87% of the surname weight. Drawn independently:
| Combination | Probability |
|---|---|
| Turkish given + non-Turkish surname | 0.68% |
| German given + Turkish surname | 0.87% |
| Turkish given + Turkish surname | 0.006% |
So roughly 1.5% of German cards cross the layer and only six in a hundred thousand produce a coherent Turkish-German pair — Mehmet Müller is about 100 times more likely than Mehmet Öztürk. The same happens wherever a corpus contains more than one naming stratum: an Arabic given name lands on an Ashkenazi surname in he_IL, Aidan Paun comes out of en_IE. The fix is to group corpora into strata and draw the pair from one stratum; it is an open debt, not a plan we have executed. See The Missing Migration Layer.
Streets are not tied to cities
Street and city are independent draws within a locale. Twelve consecutive Ukrainian cards produce вул. Захисників України, 154 in Сімферополь and вул. Гетьмана Павла Скоропадського, 161 in Київ — plausible enough — but nothing prevents a street that exists only in one city from being placed in another, or a name that belongs to a square rather than a street from appearing with a house number. The pairing that we do enforce is city↔postcode; street↔city we do not.
Given-name weights in 56 locales are calibrated, not measured
Only eight locales have given-name weights from birth registries. The other 56 were tuned toward an assumed "20–35% top-10 share everywhere" band — a norm that does not exist. Measured against the six locales that had real counts when the comparison was run in July 2026: real top-10 concentration spans 9.88–33.32% (a 3.4-fold range), our calibrated corpora span 20.68–24.84% (1.20-fold), and the Spearman rank correlation between our figures and the real ones is 0.056. That is not a compressed signal; it is no country signal at all. Individual errors reach 13 pp — real US female top-10 is 9.88%, our corpus says 23.01%. The calibration also flattened the real male/female concentration gap from 7.41 pp to 1.81 pp. Full workings in The Collapse of Given-Name Diversity.
20 locales have no city weights
44 of 64 locality files carry a population column. The remaining 20 draw cities uniformly. Some of those are queued work; some are deliberate. Where a country's statistics office publishes only regional figures, or where the available numbers mix incompatible units — Norwegian kommune against tettsted, Saudi governorate against city — we left the locale uniform rather than assemble a column from two kinds of thing. A uniformly-drawn city is a documented approximation; a column where two thirds of the rows are one unit and one third another is a defect nobody, including us, would ever see.
Cohorts cover 10 locales of 64
Fifty-four locales draw a given name with no reference to the card's birth year at all. In those, a card born in 1945 and a card born in 2005 draw from the same list.
Faker is still the floor
Where a dataset is missing, the field falls back to the Faker library — abandoned since 2020, throwing deprecations on PHP 8.5, and the source of the phone-number and address-format defects described above. It is deliberately never reached when the corpora are built, but it is still the floor under every field.
Why any of this matters to someone using the site
Two of these properties are the ones a user actually feels.
The permalink works forever. A card's URL is a promise: the name, address, phone and date of birth on it will be identical in five years. That is why the birth year is frozen in the id, why the mail domain comes from a versioned archive, and why every selection function is disciplined down to exactly one draw. Any of those three missing, and the URL silently becomes a different person — and silently is the operative word, because nothing in a card ever looks broken. It just is not the card that was bookmarked.
Realism is a distributional property, not a per-card one. Every uniformly-drawn Brazilian card is a real city with a correct postcode. The set of them describes a Brazil where São Paulo is the same size as Palmas. No amount of inspecting individual cards reveals that, which is why the limitations above are stated as measurements rather than impressions — the ones we caught, we caught by counting, and several of them were things we had assumed were fine.
Data as of 2026-07-18
Every count, share and example in this article was measured on that date directly from the running code and the shipped data files: 64 locale directories holding 261 weight files and 20 cohort files, 64 locality tables and 63 street tables. The city-draw and prefix counts were re-measured on 22 July 2026.
Card examples were generated by running the live identity function against the shipped corpora, on the same seeding path the live site uses. Top-of-corpus tables were computed by decoding the cumulative weight files and sorting by weight; they are the corpora's own numbers, which for the twelve locales listed are the registries' own counts.
Registry sources behind the weighted corpora are documented per-locale in the companion articles: Top 10 Surnames by Country, Surname Concentration Curves, and Open Data on Surname Registries.