import { countryFromText } from "@dci/core"; /** * Curated list of data-center cities / metros (≈ 300) with their region and ISO-2 country, plus US states and * Canadian provinces. Used to locate an announcement from free text. Ambiguous names (Birmingham, Lancaster…) * are deliberately absent: they are only resolved through an explicit "City, State" pattern. */ export interface CityEntry { city: string; region?: string; country: string; aliases?: string[] } const c = (city: string, country: string, region?: string, aliases?: string[]): CityEntry => ({ city, country, region, aliases }); export const CITIES: CityEntry[] = [ // United States — Virginia / Mid-Atlantic c("Ashburn", "US", "Virginia"), c("Leesburg", "US", "Virginia"), c("Sterling", "US", "Virginia"), c("Manassas", "US", "Virginia"), c("Gainesville", "US", "Virginia"), c("Haymarket", "US", "Virginia"), c("Culpeper", "US", "Virginia"), c("Richmond", "US", "Virginia"), c("Henrico County", "US", "Virginia", ["Henrico"]), c("Chesterfield County", "US", "Virginia", ["Chesterfield"]), c("Spotsylvania County", "US", "Virginia", ["Spotsylvania"]), c("Stafford County", "US", "Virginia", ["Stafford"]), c("Louisa County", "US", "Virginia", ["Louisa"]), c("Loudoun County", "US", "Virginia", ["Loudoun"]), c("Prince William County", "US", "Virginia", ["Prince William"]), c("Fauquier County", "US", "Virginia", ["Fauquier"]), c("Fairfax County", "US", "Virginia", ["Fairfax"]), c("Mecklenburg County", "US", "Virginia"), c("Pittsylvania County", "US", "Virginia"), c("Frederick", "US", "Maryland"), c("Baltimore", "US", "Maryland"), c("Quantico", "US", "Virginia"), // Georgia / Southeast c("Atlanta", "US", "Georgia"), c("Douglasville", "US", "Georgia"), c("Lithia Springs", "US", "Georgia"), c("Fayetteville", "US", "Georgia"), c("Coweta County", "US", "Georgia"), c("Newton County", "US", "Georgia"), c("Bartow County", "US", "Georgia"), c("Charlotte", "US", "North Carolina"), c("Forest City", "US", "North Carolina"), c("Maiden", "US", "North Carolina"), c("Nashville", "US", "Tennessee"), c("Memphis", "US", "Tennessee"), c("Clarksville", "US", "Tennessee"), c("Huntsville", "US", "Alabama"), c("Bessemer", "US", "Alabama"), c("Jackson", "US", "Mississippi"), c("Meridian", "US", "Mississippi"), c("New Orleans", "US", "Louisiana"), c("Richland Parish", "US", "Louisiana"), c("Little Rock", "US", "Arkansas"), c("Miami", "US", "Florida"), c("Tampa", "US", "Florida"), c("Jacksonville", "US", "Florida"), c("Orlando", "US", "Florida"), c("Greenville", "US", "South Carolina"), c("Spartanburg", "US", "South Carolina"), // Texas / South Central c("Dallas", "US", "Texas"), c("Fort Worth", "US", "Texas"), c("Garland", "US", "Texas"), c("Richardson", "US", "Texas"), c("Plano", "US", "Texas"), c("Irving", "US", "Texas"), c("Red Oak", "US", "Texas"), c("Midlothian", "US", "Texas"), c("Denton", "US", "Texas"), c("Houston", "US", "Texas"), c("San Antonio", "US", "Texas"), c("Austin", "US", "Texas"), c("Hutto", "US", "Texas"), c("Taylor", "US", "Texas"), c("Temple", "US", "Texas"), c("Abilene", "US", "Texas"), c("Amarillo", "US", "Texas"), c("Lubbock", "US", "Texas"), c("El Paso", "US", "Texas"), c("Corsicana", "US", "Texas"), c("Odessa", "US", "Texas"), c("Milam County", "US", "Texas"), c("Ellis County", "US", "Texas"), c("Oklahoma City", "US", "Oklahoma"), c("Pryor", "US", "Oklahoma"), c("Tulsa", "US", "Oklahoma"), c("Stillwater", "US", "Oklahoma"), // Midwest c("Chicago", "US", "Illinois"), c("Elk Grove Village", "US", "Illinois"), c("Northlake", "US", "Illinois"), c("Hoffman Estates", "US", "Illinois"), c("DeKalb", "US", "Illinois"), c("Yorkville", "US", "Illinois"), c("Columbus", "US", "Ohio"), c("New Albany", "US", "Ohio"), c("Hilliard", "US", "Ohio"), c("Cincinnati", "US", "Ohio"), c("Cleveland", "US", "Ohio"), c("Mount Pleasant", "US", "Wisconsin"), c("Kenosha", "US", "Wisconsin"), c("Port Washington", "US", "Wisconsin"), c("Minneapolis", "US", "Minnesota"), c("Rosemount", "US", "Minnesota"), c("Farmington", "US", "Minnesota"), c("Indianapolis", "US", "Indiana"), c("Lebanon", "US", "Indiana"), c("New Carlisle", "US", "Indiana"), c("Fort Wayne", "US", "Indiana"), c("Omaha", "US", "Nebraska"), c("Papillion", "US", "Nebraska"), c("Council Bluffs", "US", "Iowa"), c("Altoona", "US", "Iowa"), c("Des Moines", "US", "Iowa"), c("West Des Moines", "US", "Iowa"), c("Waukee", "US", "Iowa"), c("Cedar Rapids", "US", "Iowa"), c("Kansas City", "US", "Missouri"), c("De Soto", "US", "Kansas"), c("St. Louis", "US", "Missouri", ["Saint Louis"]), c("Detroit", "US", "Michigan"), c("Saline Township", "US", "Michigan"), c("Grand Rapids", "US", "Michigan"), c("Louisville", "US", "Kentucky"), // Mountain / West c("Phoenix", "US", "Arizona"), c("Mesa", "US", "Arizona"), c("Chandler", "US", "Arizona"), c("Goodyear", "US", "Arizona"), c("Buckeye", "US", "Arizona"), c("Tucson", "US", "Arizona"), c("Glendale", "US", "Arizona"), c("Surprise", "US", "Arizona"), c("Reno", "US", "Nevada"), c("Las Vegas", "US", "Nevada"), c("Henderson", "US", "Nevada"), c("Salt Lake City", "US", "Utah"), c("West Jordan", "US", "Utah"), c("Eagle Mountain", "US", "Utah"), c("Denver", "US", "Colorado"), c("Cheyenne", "US", "Wyoming"), c("Boise", "US", "Idaho"), c("Kuna", "US", "Idaho"), c("Albuquerque", "US", "New Mexico"), c("Los Lunas", "US", "New Mexico"), c("Santa Teresa", "US", "New Mexico"), // Pacific c("Santa Clara", "US", "California"), c("San Jose", "US", "California"), c("Silicon Valley", "US", "California"), c("Los Angeles", "US", "California"), c("El Segundo", "US", "California"), c("Sacramento", "US", "California"), c("Rancho Cordova", "US", "California"), c("San Francisco", "US", "California"), c("Hillsboro", "US", "Oregon"), c("Prineville", "US", "Oregon"), c("The Dalles", "US", "Oregon"), c("Umatilla", "US", "Oregon"), c("Boardman", "US", "Oregon"), c("Portland", "US", "Oregon"), c("Quincy", "US", "Washington"), c("Moses Lake", "US", "Washington"), c("Wenatchee", "US", "Washington"), c("Seattle", "US", "Washington"), c("Grant County", "US", "Washington"), // Northeast c("Boston", "US", "Massachusetts"), c("New York", "US", "New York", ["New York City", "NYC"]), c("Secaucus", "US", "New Jersey"), c("Piscataway", "US", "New Jersey"), c("Newark", "US", "New Jersey"), c("Carteret", "US", "New Jersey"), c("Philadelphia", "US", "Pennsylvania"), c("Pittsburgh", "US", "Pennsylvania"), c("Homer City", "US", "Pennsylvania"), c("Berwick", "US", "Pennsylvania"), c("Washington, D.C.", "US", "District of Columbia", ["Washington, DC", "Washington D.C."]), // Canada c("Montreal", "CA", "Quebec", ["Montréal"]), c("Quebec City", "CA", "Quebec", ["Québec City"]), c("Beauharnois", "CA", "Quebec"), c("Lévis", "CA", "Quebec", ["Levis"]), c("Toronto", "CA", "Ontario"), c("Markham", "CA", "Ontario"), c("Vaughan", "CA", "Ontario"), c("Mississauga", "CA", "Ontario"), c("Ottawa", "CA", "Ontario"), c("Calgary", "CA", "Alberta"), c("Edmonton", "CA", "Alberta"), c("Red Deer", "CA", "Alberta"), c("Grande Prairie", "CA", "Alberta"), c("Vancouver", "CA", "British Columbia"), c("Kamloops", "CA", "British Columbia"), c("Winnipeg", "CA", "Manitoba"), c("Halifax", "CA", "Nova Scotia"), // United Kingdom & Ireland c("London", "GB", "England"), c("Slough", "GB", "England"), c("Hayes", "GB", "England"), c("Manchester", "GB", "England"), c("Newport", "GB", "Wales"), c("Cardiff", "GB", "Wales"), c("Didcot", "GB", "England"), c("Hemel Hempstead", "GB", "England"), c("Cambridge", "GB", "England"), c("Hertfordshire", "GB", "England"), c("Buckinghamshire", "GB", "England"), c("Teesside", "GB", "England"), c("Blyth", "GB", "England"), c("Ravenscraig", "GB", "Scotland"), c("Edinburgh", "GB", "Scotland"), c("Glasgow", "GB", "Scotland"), c("Dublin", "IE", "Leinster"), c("Clondalkin", "IE", "Leinster"), c("Grange Castle", "IE", "Leinster"), c("Ennis", "IE", "Munster"), c("Cork", "IE", "Munster"), // Continental Europe c("Frankfurt", "DE", "Hesse", ["Frankfurt am Main"]), c("Hanau", "DE", "Hesse"), c("Offenbach", "DE", "Hesse"), c("Berlin", "DE", "Berlin"), c("Munich", "DE", "Bavaria", ["München"]), c("Hamburg", "DE", "Hamburg"), c("Düsseldorf", "DE", "North Rhine-Westphalia", ["Dusseldorf"]), c("Magdeburg", "DE", "Saxony-Anhalt"), c("Amsterdam", "NL", "North Holland"), c("Schiphol", "NL", "North Holland"), c("Haarlemmermeer", "NL", "North Holland"), c("Middenmeer", "NL", "North Holland"), c("Eemshaven", "NL", "Groningen"), c("Groningen", "NL", "Groningen"), c("Almere", "NL", "Flevoland"), c("Zeewolde", "NL", "Flevoland"), c("Paris", "FR", "Île-de-France"), c("Marseille", "FR", "Provence-Alpes-Côte d'Azur"), c("Lyon", "FR", "Auvergne-Rhône-Alpes"), c("Lisses", "FR", "Île-de-France"), c("Bordeaux", "FR", "Nouvelle-Aquitaine"), c("Fos-sur-Mer", "FR", "Provence-Alpes-Côte d'Azur"), c("Madrid", "ES", "Community of Madrid"), c("Barcelona", "ES", "Catalonia"), c("Aragon", "ES", "Aragon", ["Aragón"]), c("Zaragoza", "ES", "Aragon"), c("Milan", "IT", "Lombardy", ["Milano"]), c("Rome", "IT", "Lazio"), c("Turin", "IT", "Piedmont"), c("Lisbon", "PT", "Lisbon"), c("Sines", "PT", "Setúbal"), c("Porto", "PT", "Porto"), c("Stockholm", "SE", "Stockholm"), c("Gävle", "SE", "Gävleborg"), c("Luleå", "SE", "Norrbotten", ["Lulea"]), c("Boden", "SE", "Norrbotten"), c("Oslo", "NO", "Oslo"), c("Skien", "NO", "Telemark"), c("Hamar", "NO", "Innlandet"), c("Rjukan", "NO", "Telemark"), c("Copenhagen", "DK", "Capital Region"), c("Odense", "DK", "Southern Denmark"), c("Fredericia", "DK", "Southern Denmark"), c("Helsinki", "FI", "Uusimaa"), c("Hamina", "FI", "Kymenlaakso"), c("Espoo", "FI", "Uusimaa"), c("Kajaani", "FI", "Kainuu"), c("Reykjavik", "IS", undefined, ["Reykjavík"]), c("Keflavik", "IS", undefined, ["Keflavík"]), c("Brussels", "BE", "Brussels"), c("Zurich", "CH", "Zurich", ["Zürich"]), c("Geneva", "CH", "Geneva"), c("Vienna", "AT", "Vienna"), c("Warsaw", "PL", "Masovia"), c("Prague", "CZ", "Prague"), c("Athens", "GR", "Attica"), c("Bucharest", "RO", "Bucharest"), c("Istanbul", "TR", "Istanbul"), c("Luxembourg City", "LU", undefined, ["Luxembourg"]), // Middle East & Africa c("Dubai", "AE", "Dubai"), c("Abu Dhabi", "AE", "Abu Dhabi"), c("Riyadh", "SA", "Riyadh"), c("Dammam", "SA", "Eastern Province"), c("Jeddah", "SA", "Makkah"), c("NEOM", "SA", "Tabuk"), c("Doha", "QA"), c("Manama", "BH"), c("Muscat", "OM"), c("Tel Aviv", "IL"), c("Cairo", "EG"), c("Casablanca", "MA"), c("Lagos", "NG"), c("Nairobi", "KE"), c("Johannesburg", "ZA", "Gauteng"), c("Cape Town", "ZA", "Western Cape"), c("Accra", "GH"), // Asia-Pacific c("Singapore", "SG"), c("Johor Bahru", "MY", "Johor"), c("Johor", "MY", "Johor"), c("Sedenak", "MY", "Johor"), c("Kulai", "MY", "Johor"), c("Cyberjaya", "MY", "Selangor"), c("Kuala Lumpur", "MY"), c("Batam", "ID", "Riau Islands"), c("Jakarta", "ID"), c("Bekasi", "ID", "West Java"), c("Bangkok", "TH"), c("Chonburi", "TH"), c("Manila", "PH"), c("Ho Chi Minh City", "VN"), c("Hanoi", "VN"), c("Tokyo", "JP"), c("Inzai", "JP", "Chiba"), c("Osaka", "JP"), c("Hokkaido", "JP", "Hokkaido"), c("Seoul", "KR"), c("Incheon", "KR"), c("Busan", "KR"), c("Hong Kong", "HK"), c("Taipei", "TW"), c("Taoyuan", "TW"), c("Sydney", "AU", "New South Wales"), c("Melbourne", "AU", "Victoria"), c("Canberra", "AU", "Australian Capital Territory"), c("Perth", "AU", "Western Australia"), c("Brisbane", "AU", "Queensland"), c("Auckland", "NZ"), c("Mumbai", "IN", "Maharashtra"), c("Navi Mumbai", "IN", "Maharashtra"), c("Chennai", "IN", "Tamil Nadu"), c("Hyderabad", "IN", "Telangana"), c("Bengaluru", "IN", "Karnataka", ["Bangalore"]), c("Pune", "IN", "Maharashtra"), c("Noida", "IN", "Uttar Pradesh"), c("Delhi", "IN", "Delhi", ["New Delhi"]), c("Kolkata", "IN", "West Bengal"), c("Visakhapatnam", "IN", "Andhra Pradesh"), c("Shanghai", "CN"), c("Beijing", "CN"), c("Shenzhen", "CN", "Guangdong"), c("Ulanqab", "CN", "Inner Mongolia"), c("Zhangjiakou", "CN", "Hebei"), c("Guizhou", "CN", "Guizhou"), // Latin America c("São Paulo", "BR", "São Paulo", ["Sao Paulo"]), c("Campinas", "BR", "São Paulo"), c("Rio de Janeiro", "BR", "Rio de Janeiro"), c("Fortaleza", "BR", "Ceará"), c("Santiago", "CL"), c("Bogotá", "CO", undefined, ["Bogota"]), c("Querétaro", "MX", "Querétaro", ["Queretaro"]), c("Mexico City", "MX"), c("Monterrey", "MX", "Nuevo León"), c("Buenos Aires", "AR"), c("Lima", "PE"), c("Montevideo", "UY"), ]; export const US_STATES: Record = { AL: "Alabama", AK: "Alaska", AZ: "Arizona", AR: "Arkansas", CA: "California", CO: "Colorado", CT: "Connecticut", DE: "Delaware", FL: "Florida", GA: "Georgia", HI: "Hawaii", ID: "Idaho", IL: "Illinois", IN: "Indiana", IA: "Iowa", KS: "Kansas", KY: "Kentucky", LA: "Louisiana", ME: "Maine", MD: "Maryland", MA: "Massachusetts", MI: "Michigan", MN: "Minnesota", MS: "Mississippi", MO: "Missouri", MT: "Montana", NE: "Nebraska", NV: "Nevada", NH: "New Hampshire", NJ: "New Jersey", NM: "New Mexico", NY: "New York", NC: "North Carolina", ND: "North Dakota", OH: "Ohio", OK: "Oklahoma", OR: "Oregon", PA: "Pennsylvania", RI: "Rhode Island", SC: "South Carolina", SD: "South Dakota", TN: "Tennessee", TX: "Texas", UT: "Utah", VT: "Vermont", VA: "Virginia", WA: "Washington", WV: "West Virginia", WI: "Wisconsin", WY: "Wyoming", DC: "District of Columbia", }; export const CA_PROVINCES: Record = { AB: "Alberta", BC: "British Columbia", MB: "Manitoba", NB: "New Brunswick", NL: "Newfoundland and Labrador", NS: "Nova Scotia", ON: "Ontario", PE: "Prince Edward Island", QC: "Quebec", SK: "Saskatchewan" }; const STATE_NAMES = new Map(); for (const [ab, n] of Object.entries(US_STATES)) { STATE_NAMES.set(n.toLowerCase(), { region: n, country: "US" }); STATE_NAMES.set(ab, { region: n, country: "US" }); } for (const [ab, n] of Object.entries(CA_PROVINCES)) { STATE_NAMES.set(n.toLowerCase(), { region: n, country: "CA" }); STATE_NAMES.set(ab, { region: n, country: "CA" }); } STATE_NAMES.set("québec", { region: "Quebec", country: "CA" }); function escapeRe(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } const cityPatterns: Array<{ entry: CityEntry; re: RegExp }> = CITIES.flatMap((entry) => [entry.city, ...(entry.aliases ?? [])].map((a) => ({ entry, re: new RegExp(`(? b.re.source.length - a.re.source.length); const stateAlternation = [...Object.values(US_STATES), ...Object.values(CA_PROVINCES), "Québec", ...Object.keys(US_STATES), ...Object.keys(CA_PROVINCES)].sort((a, b) => b.length - a.length).map(escapeRe).join("|"); /** "Frederick, Maryland" / "Frederick, MD" / "Frederick, Md." — the city token must look like a proper noun. */ const CITY_STATE_RE = new RegExp(`\\b([A-Z][a-zA-Z.'-]+(?:[ -][A-Z][a-zA-Z.'-]+){0,3}),\\s*(${stateAlternation})(?![a-z])\\.?(?![A-Za-z])`, "g"); /** Standalone state / province name ("in Virginia", "Texas-based"). */ const STATE_RE = new RegExp(`(? b.length - a.length).map(escapeRe).join("|")})(?![a-z])`, "g"); export interface LocationHit { city: string | null; region: string | null; country: string | null; method: string; index: number } const NOT_A_CITY = /^(The|A|An|In|At|Of|By|From|Data|Center|Centre|County|Inc|LLC|Corp|Group|North|South|East|West|Northern|Southern|Central|New|Greater|Downtown|Rural|Metro|AI|MW|GW|US|U\.S\.|Today|Yesterday|Tomorrow|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday|January|February|March|April|May|June|July|August|September|October|November|December|Gov|Governor|Sen|Senator|Rep|Mayor|President|CEO|Inc\.|Company|Energy|Power|Electric|Utility|University|College|State|Commonwealth|Republic|Kingdom|Government|Department|Board|Council|Commission)$/; /** Best location in a text: explicit "City, State" first, then curated cities, then standalone states, then countries. */ export function detectLocation(text: string): LocationHit | null { if (!text) return null; CITY_STATE_RE.lastIndex = 0; for (const m of text.matchAll(CITY_STATE_RE)) { // "54MW Data Center In Dallas, Texas": drop leading words that cannot start a city name until a proper noun is left const words = m[1]!.replace(/\.$/, "").split(/[ ]/); while (words.length > 1 && (NOT_A_CITY.test(words[0]!) || /^(Near|Outside|Around|Into|Onto|Across|Through|Plans?|Planned|Proposed|Approves?|Approved|Files?|Builds?|Opens?|Campus|Facility|Site|Project|Hub|Park)$/i.test(words[0]!))) words.shift(); const city = words.join(" "); const st = m[2]!; const stateInfo = STATE_NAMES.get(st.length <= 2 ? st : st.toLowerCase()); if (!stateInfo || NOT_A_CITY.test(city)) continue; // "County, Virginia" is fine, but "Virginia, USA"-style matches are not a city if (STATE_NAMES.has(city.toLowerCase())) continue; return { city: city.replace(/\b(city|town) of\b\s*/i, ""), region: stateInfo.region, country: stateInfo.country, method: "regex:city_state", index: m.index ?? 0 }; } // earliest hit wins, but a proper city beats a county / parish named earlier in the same text let best: { entry: CityEntry; index: number; county: boolean } | null = null; for (const { entry, re } of cityPatterns) { const m = re.exec(text); if (!m) continue; const county = /\b(County|Parish|Township)$/.test(entry.city); if (!best || (county === best.county ? m.index < best.index : !county)) best = { entry, index: m.index, county }; } if (best) return { city: best.entry.city, region: best.entry.region ?? null, country: best.entry.country, method: "lexicon:city", index: best.index }; STATE_RE.lastIndex = 0; const sm = STATE_RE.exec(text); if (sm) { const info = STATE_NAMES.get(sm[1]!.toLowerCase()); // "Georgia" alone is the US state unless the text is clearly about the country if (info && !(info.region === "Georgia" && /\b(Tbilisi|Republic of Georgia|Georgian government|Caucasus)\b/.test(text))) return { city: null, region: info.region, country: info.country, method: "lexicon:state", index: sm.index }; } const country = countryFromTextSafe(text); if (country) return { city: null, region: null, country, method: "lexicon:country", index: 0 }; return null; } /** countryFromText with the traps removed: "Latin/North America" is not the US, "Georgia" is a US state, "Jordan" is a person. */ export function countryFromTextSafe(text: string | null | undefined): string | null { if (!text) return null; const cleaned = text .replace(/\b(Latin|North|South|Central) America(n)?\b/g, " ") .replace(/\bAmericas\b/g, " ") .replace(/\bGeorgia\b/g, (m) => (/\b(Tbilisi|Republic of Georgia|Caucasus)\b/.test(text) ? m : " ")) .replace(/\b[A-Z][a-z]+ Jordan\b/g, " "); return countryFromText(cleaned); }