I’ve spent enough time walking around different cities to notice a pattern: certain names keep showing up on street signs, over and over, regardless of which town you’re in. In Turkey, it’s Atatürk — Atatürk Caddesi, Atatürk Bulvarı, Atatürk Mahallesi, sometimes three of them in the same city. In Poland, it’s Jan Paweł II, or Kościuszko, or Piłsudski, competing for the same honor.
That's a hypothesis, not a fact: each country tends to concentrate its street-naming around a small set of iconic historical figures, rather than spreading credit widely. I wanted to know if it actually held up, so I pulled real data and tested it — this post walks through how I did it and what I found, including a moment when I almost fooled myself.
(Code for all of this is on GitHub — link at the end.)
OpenStreetMap is the obvious candidate: it’s free, it’s global, and every named road is tagged with a `name` field, regardless of country. The Overpass API lets you query it directly without downloading a full country extract, which matters when some of these queries return hundreds of thousands of rows.
The query I used pulls just the `name` tag off every `highway` way in a country — no geometry, which keeps things fast even at country scale:
QUERY_TEMPLATE = """
[out:json][timeout:180];
area["ISO3166-1"="{iso}"][admin_level=2]->.searchArea;
(
way["highway"]["name"](area.searchArea);
);
out tags;
"""
def fetch_country(iso: str, max_retries: int = 5) -> list[str]:
query = QUERY_TEMPLATE.format(iso=iso)
last_error = None
for attempt in range(max_retries):
try:
resp = requests.get(
OVERPASS_URL, params={"data": query}, headers=HEADERS, timeout=200
)
resp.raise_for_status()
elements = resp.json()["elements"]
return [
el["tags"]["name"] for el in elements if "tags" in el and "name" in el["tags"]
]
except (requests.HTTPError, requests.exceptions.JSONDecodeError) as exc:
last_error = exc
wait = 5 * (attempt + 1)
time.sleep(wait)
raise RuntimeError(f"[{iso}] failed after {max_retries} attempts") from last_error
Nothing fancy — it’s a shared public API, so I built in retries with backoff since it occasionally throttles or times out under load. Run it for a country code, and it caches the result to a CSV so you’re not re-querying every time you tweak the analysis downstream.
For Poland, this pulled 876,727 named streets. For Turkey, 424,023. Dataset size varies a lot by country — France turned out to be the biggest by far at 3.84 million named streets, more than double England’s or Spain’s (~1.8 million each), which meant bumping the query’s timeout budget up considerably before it would reliably finish.
The harder problem isn’t fetching the data — it’s deciding which streets are “named after a person” and which person. Street naming conventions vary a lot by language: Polish uses the genitive case (“Tadeusza Kościuszki,” not “Tadeusz Kościuszko”), Turkish appends a suffix word (“Atatürk Bulvarı” — “Atatürk Boulevard”), and there’s no clean, universal API for “list of historical figures by country.”
So I built a small curated config instead — a YAML file mapping each country to a list of figures and the string patterns (including case/spelling variants) that would show up in a street name referencing them:
countries:
- name: Poland
iso: PL
figures:
- name: Tadeusz Kościuszko
patterns: ["kościuszki", "kosciuszki", "kościuszko"]
- name: Jan Paweł II
patterns: ["pawła ii", "pawla ii"]
- name: Józef Piłsudski
patterns: ["piłsudski", "pilsudski"]
And the matching itself is just a substring search per figure:
def match_country(iso: str, country_cfg: dict) -> pd.DataFrame:
df = pd.read_csv(RAW_DIR / f"{iso}.csv")
names_lower = df["street_name"].str.lower()
total_streets = len(df)
rows = []
for figure in country_cfg["figures"]:
mask = names_lower.str.contains(
"|".join(figure["patterns"]), case=False, regex=True, na=False
)
count = int(mask.sum())
rows.append({
"figure": figure["name"],
"street_count": count,
"share_of_named_streets": count / total_streets if total_streets else 0,
})
return pd.DataFrame(rows).sort_values("street_count", ascending=False)
With a first-pass list of just three Polish figures, I got:

Kościuszko, Jan Paweł II, and Piłsudski, roughly where I expected. But this raised an obvious problem.
I picked those three names because I already believed they’d rank highly. That’s not a test of the hypothesis; that’s just counting the things I assumed would be true. If I only ever check the names I already suspect are common, I will always “confirm” my hypothesis — regardless of whether it’s actually correct.
So I built a second script whose entire job is to argue with me: it looks at every street name in the country, strips out the generic words (street/avenue/suffix words specific to that language), keeps whatever looks like a multi-word personal name, and shows me the most common ones that aren’t already in my configured list.
def strip_generic(name: str, generic_words: list[str], position: str) -> str:
if position == "none" or not generic_words:
return name
tokens = name.split()
generic_set = {w.lower() for w in generic_words}
if position == "suffix" and tokens[-1].lower().strip(".") in generic_set:
tokens = tokens[:-1]
elif position == "prefix" and tokens[0].lower() in generic_set:
tokens = tokens[1:]
return " ".join(tokens)
Running this against the Polish data surfaced a list of names I hadn’t accounted for — real, prominent historical figures my seed list had simply missed: Adam Mickiewicz, Marie Curie (Maria Skłodowska-Curie), Henryk Sienkiewicz, Kazimierz Wielki, Mikołaj Kopernik (Copernicus), and more.
That’s a legitimate hole in the hypothesis test. So I expanded the config to 19 Polish figures and re-ran the whole pipeline.
The top 3 held. Kościuszko and Jan Paweł II are still #1 and #2 — separated by only 205 streets out of nearly a million, essentially a statistical coin flip — with Piłsudski still #3, comfortably ahead of #4 (Mickiewicz, at 4,252).


That’s a much more honest result — I checked myself, found real gaps, and the original hypothesis survived the correction rather than being propped up by cherry-picked evidence.
I ran the identical pipeline for Turkey, starting with a broader seed list of ~15 figures spanning Ottoman sultans, Republic-era leaders, and cultural figures, then ran the same “argue with me” discovery step. It caught a few real misses on the Turkish side too — Yunus Emre (13th-century poet), Kazım Karabekir, Alparslan Türkeş, Zübeyde Hanım (Atatürk’s mother), and journalist Uğur Mumcu all got added to the list before I trusted the results.
One thing the discovery step also caught, indirectly: Turkish text has a well-known Unicode trap. Python’s default .lower() turns the Turkish capital "İ" into a lowercase "i" plus a separate combining dot character, not a plain "i" — so a naive lowercase-and-substring-match silently fails to match anything starting with İ, including İnönü. I only noticed because İsmet İnönü kept turning up in the "unmatched" list despite being in my config. The fix is a one-line normalization before lowering:
names_lower = df["street_name"].str.replace("İ", "i", regex=False).str.lower() Small thing, but it’s exactly the kind of silent bug that would have quietly undercounted a major figure without anyone noticing.
With that fixed, the Turkish results:


This is a genuinely different shape than Poland. In Poland, the top three are clustered tightly together — Kościuszko barely edges out Jan Paweł II. In Turkey, Atatürk isn’t just #1, he’s in a different league entirely: nearly 4,500 streets, close to 4x the runner-up (İnönü, at 1,193), and more than the next five names combined.
Same hypothesis, same method, two very different national patterns: Poland spreads its street-naming credit across several 19th/20th-century figures fairly evenly; Turkey concentrates it overwhelmingly on one.
Poland and Turkey were the two countries that started this whole idea, but a hypothesis tested on two data points isn’t much of a hypothesis. I ran the same pipeline — fetch, discover, match, visualize — against Portugal, Spain, England, and France to see whether “one small set of names dominates” is a universal pattern or just something I’d noticed in two specific places.
The discovery step earned its keep again: for Portugal, Spain, and England it came back mostly with generic/religious/descriptive names (churches, saints, “The Crescent,” “Station Approach”) — nothing that looked like a missed historical figure. For France, it caught one real gap: Général Leclerc, the WWII liberator of Paris, wasn’t in my original config despite showing up on 4,836 streets. Added, and re-ran.
Portugal (414,382 named streets):


Spain (1,823,403 named streets):


One number from the Spanish results is worth calling out on its own: Francisco Franco — 6 streets, out of nearly two million named ways. That’s not a matching failure; it’s a real historical signal. Spain’s 2007 “Ley de Memoria Histórica” required municipalities to remove Francoist-era street names, and the data shows it. A street-naming dataset isn’t just about who a country celebrates — it also shows what a country has deliberately un-named.
England (1,830,830 named streets):


France (3,840,136 named streets — by far the largest dataset in this project, more than double any other country here):


Lining up the #1-vs-#2 margin across all six countries makes the real finding visible: this isn’t one universal pattern, it’s two different ones.

Turkey and France have a runaway #1 — a single figure (a founding father, a WWII/postwar leader) who dwarfs everyone else. Poland, Portugal, and Spain instead spread credit across several comparably-ranked historical and cultural figures, with no single name pulling far ahead. My original hypothesis was really only half right: countries do concentrate street-naming around a small set of figures — but whether that set has one obvious winner or three-to-five co-leaders depends on the country’s specific history, not on some universal rule.
I don’t want to oversell what substring matching can do:
The whole pipeline — fetch, discover, match, visualize — is on GitHub:
It’s four small scripts and one YAML config file, with Turkey, Poland, France, Portugal, Spain, and England already configured as examples. Running it for a new country is:
python src/fetch_streets.py <ISO_CODE>
python src/discover_candidates.py <ISO_CODE> # check what's missing
python src/match_figures.py <ISO_CODE>
python src/visualize.py <ISO_CODE>
If you try it for your own country, I’d genuinely like to know what you find — especially if the discovery step catches something a “confirm your bias” version of this analysis would have missed. Drop your top 3 in the comments, or open an issue on the repo with your country’s config.
Thank you for joining us on this insightful journey, and we look forward to accompanying you in your future coding endeavors. Happy Coding! If you have any questions or want to contact me, you can find my social media accounts at the link below.
You can also follow my other blog posts on my website below.
<hr><p>Do Countries Really Name Their Streets After the Same Handful of Heroes? was originally published in Artificial Intelligence in Plain English on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>