#!/usr/bin/env python3 """Build data/world-names.json from curated regional lists + optional bulk file. Usage: .venv/bin/python scripts/build_world_names.py .venv/bin/python scripts/build_world_names.py --bulk /path/to/first-names.txt Bulk file: one name per line (e.g. NameDatabases first names). Tagged intl/en. Curated regional tags always merge on top. """ from __future__ import annotations import argparse import json import re from pathlib import Path ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / "stork" / "data" / "world-names.json" OUT_REPO = ROOT / "data" / "world-names.json" RAW: dict[str, dict[str, object]] = {} LATIN = re.compile(r"^[A-Za-z][A-Za-z'\-]{0,39}$") def add(names: list[str], regions: list[str], gender: str = "u") -> None: for raw in names: name = raw.strip().strip("\ufeff") if not name or len(name) > 40: continue if not LATIN.match(name) and not any(ord(c) > 127 for c in name): continue # Prefer ASCII-friendly bulk; allow unicode from curated if any(ord(c) > 127 for c in name) and "ru" not in regions and "he" not in regions and "ar" not in regions: # keep non-latin only when curated with a region if set(regions) <= {"intl", "en"}: continue key = name.casefold() display = name.title() if name.isascii() and all(c.isalpha() or c in "'-" for c in name) else name if key not in RAW: RAW[key] = {"name": display, "regions": set(), "gender": gender} entry = RAW[key] assert isinstance(entry["regions"], set) entry["regions"].update(regions) if gender in {"f", "m"} and entry.get("gender") == "u": entry["gender"] = gender elif gender in {"f", "m"} and entry.get("gender") not in {gender, "u"}: entry["gender"] = "u" def seed_curated() -> None: add( [ "Emma", "Olivia", "Ava", "Sophia", "Isabella", "Mia", "Charlotte", "Amelia", "Harper", "Evelyn", "Abigail", "Emily", "Elizabeth", "Sofia", "Ella", "Madison", "Scarlett", "Victoria", "Aria", "Grace", "Chloe", "Camila", "Penelope", "Riley", "Layla", "Lillian", "Nora", "Zoey", "Mila", "Aubrey", "Hannah", "Lily", "Addison", "Eleanor", "Natalie", "Luna", "Savannah", "Brooklyn", "Leah", "Zoe", "Stella", "Hazel", "Ellie", "Paisley", "Audrey", "Skylar", "Violet", "Claire", "Bella", "Lucy", "Anna", "Caroline", "Genesis", "Aaliyah", "Kennedy", "Kinsley", "Allison", "Maya", "Sarah", "Madelyn", "Adeline", "Alexa", "Ariana", "Elena", "Gabriella", "Naomi", "Alice", "Sadie", "Hailey", "Eva", "Emilia", "Autumn", "Quinn", "Nevaeh", "Piper", "Ruby", "Serenity", "Willow", "Everly", "Cora", "Kaylee", "Lydia", "Aubree", "Arianna", "Eliana", "Peyton", "Melanie", "Gianna", "Isabelle", "Julia", "Valentina", "Nova", "Clara", "Vivian", "Reagan", "Mackenzie", "Madeline", "Brielle", "Delilah", "Isla", "Liam", "Noah", "Oliver", "James", "Elijah", "William", "Henry", "Lucas", "Benjamin", "Theodore", "Jack", "Levi", "Alexander", "Samuel", "Sebastian", "Mateo", "Daniel", "Matthew", "Owen", "Asher", "Leo", "Jackson", "David", "Joseph", "Julian", "Luke", "Michael", "Thomas", "Ezra", "Charles", "Hudson", "John", "Ryan", "Jayden", "Anthony", "Dylan", "Isaac", "Lincoln", "Nathan", "Caleb", "Aaron", "Eli", "Gabriel", "Adrian", "Landon", "Jordan", "Ian", "Carson", "Colton", "Adam", "Miles", "Zachary", "Cooper", "Xavier", "Zane", "Zion", "Zander", "Felix", "Oscar", "Ivy", "Jade", "June", "Iris", "Eden", "Rose", "Daisy", "Sage", "Quinn", "River", "Skye", ], ["en", "intl"], ) add( [ "Anya", "Anastasia", "Daria", "Dasha", "Katya", "Ekaterina", "Elena", "Lena", "Irina", "Ira", "Olga", "Olya", "Natalia", "Natasha", "Sofia", "Sonya", "Svetlana", "Sveta", "Tatiana", "Tanya", "Yulia", "Polina", "Alina", "Marina", "Vera", "Nadezhda", "Nadia", "Galina", "Valentina", "Zoya", "Zina", "Zinaida", "Zlata", "Zhanna", "Liza", "Elizaveta", "Varvara", "Varya", "Ksenia", "Oksana", "Ludmila", "Lyuda", "Raisa", "Inna", "Nina", "Tamara", "Larisa", "Alla", "Rita", "Alexei", "Alexander", "Sasha", "Andrei", "Andrey", "Anton", "Boris", "Dmitri", "Dima", "Ivan", "Vanya", "Igor", "Kirill", "Konstantin", "Kostya", "Maxim", "Misha", "Mikhail", "Nikolai", "Kolya", "Oleg", "Pavel", "Pasha", "Petr", "Pyotr", "Roman", "Sergei", "Vadim", "Viktor", "Vladimir", "Vova", "Yuri", "Yura", "Timur", "Artem", "Denis", "Ilya", "Lev", "Zakhar", "Zahar", "Gleb", "Fedor", "Fyodor", "Stepan", "Nikita", "Matvei", "Egor", "Yaroslav", "Mira", "Lara", "Klara", "Rosa", "Raya", "Tonya", "Zhenya", "Vika", "Alyosha", "Seryozha", ], ["ru", "intl"], ) add( [ "Noa", "Noah", "Maya", "Maayan", "Yael", "Talia", "Shira", "Shiri", "Lior", "Liya", "Ayala", "Adi", "Orli", "Orly", "Hila", "Hillel", "Tamar", "Rivka", "Riva", "Sarah", "Sara", "Miriam", "Miri", "Leah", "Rachel", "Esther", "Hadassah", "Chana", "Hannah", "Devorah", "Naomi", "Shoshana", "Shoshi", "Vered", "Gal", "Gali", "Noya", "Eden", "Idan", "Eitan", "Yonatan", "Yossi", "Yosef", "David", "Daniel", "Dani", "Ari", "Ariel", "Aviv", "Omri", "Omer", "Amir", "Amit", "Nadav", "Noam", "Roi", "Roy", "Shai", "Shay", "Ziv", "Zeev", "Zohar", "Tomer", "Tal", "Yuval", "Itai", "Itay", "Eyal", "Eliana", "Alon", "Alona", "Batya", "Bracha", "Chaya", "Gila", "Ilana", "Inbal", "Keren", "Liora", "Michal", "Mor", "Neta", "Ofir", "Orit", "Pnina", "Rina", "Roni", "Sigal", "Smadar", "Tehila", "Yaara", "Yasmin", "Ziva", "Zurit", "Asher", "Boaz", "Caleb", "Ezra", ], ["he", "intl"], ) add( [ "Amina", "Amira", "Fatima", "Layla", "Leila", "Noor", "Nur", "Yasmin", "Yasmine", "Zara", "Zahra", "Zainab", "Zaynab", "Hana", "Maryam", "Mariam", "Salma", "Samira", "Rania", "Rana", "Dina", "Dalia", "Lina", "Lama", "Nour", "Noura", "Farah", "Jana", "Aya", "Iman", "Huda", "Heba", "Reem", "Rima", "Sana", "Zaina", "Zayna", "Zia", "Ziana", "Aisha", "Omar", "Ahmed", "Ahmad", "Ali", "Hassan", "Youssef", "Yusuf", "Karim", "Samir", "Tariq", "Zaid", "Zayd", "Zain", "Zayn", "Yasin", "Ibrahim", "Ismail", "Khalid", "Malik", "Nabil", "Rami", "Sami", "Walid", "Yazan", "Adel", "Faisal", ], ["ar", "intl"], ) add( [ "Camille", "Chloe", "Clara", "Elise", "Juliette", "Louise", "Manon", "Margot", "Ines", "Adele", "Celeste", "Colette", "Odette", "Odet", "Simone", "Violette", "Antoine", "Louis", "Hugo", "Raphael", "Theo", "Enzo", "Pierre", "Paul", "Jacques", "Henri", "Remy", "Etienne", "Olivier", "Giulia", "Francesca", "Chiara", "Alessia", "Bianca", "Lucia", "Marco", "Luca", "Matteo", "Alessandro", "Giovanni", "Francesco", "Lorenzo", "Carmen", "Isabel", "Pilar", "Ana", "Diego", "Javier", "Carlos", "Miguel", "Pablo", "Luis", "Jorge", "Pedro", "Santiago", ], ["fr", "intl"], ) add( [ "Greta", "Ingrid", "Freya", "Freja", "Astrid", "Sigrid", "Helga", "Ebba", "Alva", "Saga", "Linnea", "Ida", "Lars", "Erik", "Anders", "Johan", "Nils", "Oskar", "Bjorn", "Sven", "Magnus", "Henrik", "Hans", "Franz", "Otto", "Klaus", "Heidi", "Anneliese", "Ursula", "Zelda", "Hilda", "Wanda", "Helene", "Anke", "Birgit", "Dagmar", "Elke", ], ["de", "intl"], ) add( [ "Zed", "Zee", "Zia", "Ziv", "Zoa", "Zoe", "Zola", "Zora", "Zoya", "Zuri", "Zana", "Zara", "Zena", "Zina", "Ziva", "Zuzu", "Zizi", "Zaza", "Zeke", "Ziggy", "Aziz", "Aziza", "Eliza", "Liza", "Izzy", "Ozzy", "Suzy", "Roza", "Roze", "Ruza", ], ["intl", "en", "ru", "he", "ar"], ) for n in ["Emma", "Olivia", "Sofia", "Maya", "Noa", "Zara", "Zena", "Zina", "Anya", "Daria"]: if n.casefold() in RAW: RAW[n.casefold()]["gender"] = "f" for n in ["Liam", "Noah", "Oliver", "Levi", "Ezra", "Ivan", "David", "Shai", "Omar", "Zane"]: if n.casefold() in RAW: RAW[n.casefold()]["gender"] = "m" def load_bulk(path: Path, limit: int = 18000) -> int: added = 0 for line in path.read_text(encoding="utf-8", errors="ignore").splitlines(): name = line.strip().strip("\ufeff") if not LATIN.match(name): continue before = len(RAW) add([name], ["intl", "en"]) if len(RAW) > before: added += 1 if added >= limit: break return added def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--bulk", type=Path, default=None, help="Optional one-name-per-line file") ap.add_argument("--limit", type=int, default=18000) args = ap.parse_args() seed_curated() if args.bulk and args.bulk.is_file(): n = load_bulk(args.bulk, limit=args.limit) print(f"Merged {n} bulk names from {args.bulk}") items = [] for entry in RAW.values(): regions = sorted(entry["regions"]) # type: ignore[arg-type] items.append( { "name": entry["name"], "regions": regions, "gender": entry.get("gender") or "u", } ) items.sort(key=lambda x: str(x["name"]).casefold()) OUT.parent.mkdir(parents=True, exist_ok=True) payload = json.dumps({"names": items}, ensure_ascii=False, separators=(",", ":")) + "\n" OUT.write_text(payload, encoding="utf-8") OUT_REPO.parent.mkdir(parents=True, exist_ok=True) OUT_REPO.write_text(payload, encoding="utf-8") print(f"Wrote {len(items)} names → {OUT}") print(f"Wrote {len(items)} names → {OUT_REPO}") return 0 if __name__ == "__main__": raise SystemExit(main())