SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
18.5 KB · 145 lines typescript
Raw Blame History
1import { countryFromText } from "@dci/core";23/**4 * Curated list of data-center cities / metros (≈ 300) with their region and ISO-2 country, plus US states and5 * Canadian provinces. Used to locate an announcement from free text. Ambiguous names (Birmingham, Lancaster…)6 * are deliberately absent: they are only resolved through an explicit "City, State" pattern.7 */8export interface CityEntry { city: string; region?: string; country: string; aliases?: string[] }910const c = (city: string, country: string, region?: string, aliases?: string[]): CityEntry => ({ city, country, region, aliases });1112export const CITIES: CityEntry[] = [13  // United States — Virginia / Mid-Atlantic14  c("Ashburn", "US", "Virginia"), c("Leesburg", "US", "Virginia"), c("Sterling", "US", "Virginia"), c("Manassas", "US", "Virginia"), c("Gainesville", "US", "Virginia"), c("Haymarket", "US", "Virginia"),15  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"]),16  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"]),17  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"),18  // Georgia / Southeast19  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"),20  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"),21  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"),22  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"),23  // Texas / South Central24  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"),25  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"),26  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"),27  c("Oklahoma City", "US", "Oklahoma"), c("Pryor", "US", "Oklahoma"), c("Tulsa", "US", "Oklahoma"), c("Stillwater", "US", "Oklahoma"),28  // Midwest29  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"),30  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"),31  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"),32  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"),33  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"),34  // Mountain / West35  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"),36  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"),37  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"),38  // Pacific39  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"),40  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"),41  c("Quincy", "US", "Washington"), c("Moses Lake", "US", "Washington"), c("Wenatchee", "US", "Washington"), c("Seattle", "US", "Washington"), c("Grant County", "US", "Washington"),42  // Northeast43  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"),44  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."]),45  // Canada46  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"),47  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"),48  // United Kingdom & Ireland49  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"),50  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"),51  c("Dublin", "IE", "Leinster"), c("Clondalkin", "IE", "Leinster"), c("Grange Castle", "IE", "Leinster"), c("Ennis", "IE", "Munster"), c("Cork", "IE", "Munster"),52  // Continental Europe53  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"),54  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"),55  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"),56  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"),57  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"),58  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"),59  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"]),60  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"]),61  // Middle East & Africa62  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"),63  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"),64  // Asia-Pacific65  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"),66  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"),67  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"),68  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"),69  c("Shanghai", "CN"), c("Beijing", "CN"), c("Shenzhen", "CN", "Guangdong"), c("Ulanqab", "CN", "Inner Mongolia"), c("Zhangjiakou", "CN", "Hebei"), c("Guizhou", "CN", "Guizhou"),70  // Latin America71  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"),72  c("Buenos Aires", "AR"), c("Lima", "PE"), c("Montevideo", "UY"),73];7475export const US_STATES: Record<string, string> = {76  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",77};78export const CA_PROVINCES: Record<string, string> = { 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" };7980const STATE_NAMES = new Map<string, { region: string; country: string }>();81for (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" }); }82for (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" }); }83STATE_NAMES.set("québec", { region: "Quebec", country: "CA" });8485function escapeRe(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }86const cityPatterns: Array<{ entry: CityEntry; re: RegExp }> = CITIES.flatMap((entry) => [entry.city, ...(entry.aliases ?? [])].map((a) => ({ entry, re: new RegExp(`(?<![A-Za-z])${escapeRe(a)}(?![a-z])`, "u") }))).sort((a, b) => b.re.source.length - a.re.source.length);8788const 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("|");89/** "Frederick, Maryland" / "Frederick, MD" / "Frederick, Md." — the city token must look like a proper noun. */90const 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");91/** Standalone state / province name ("in Virginia", "Texas-based"). */92const STATE_RE = new RegExp(`(?<![A-Za-z])(${[...Object.values(US_STATES), ...Object.values(CA_PROVINCES), "Québec"].sort((a, b) => b.length - a.length).map(escapeRe).join("|")})(?![a-z])`, "g");9394export interface LocationHit { city: string | null; region: string | null; country: string | null; method: string; index: number }9596const 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)$/;9798/** Best location in a text: explicit "City, State" first, then curated cities, then standalone states, then countries. */99export function detectLocation(text: string): LocationHit | null {100  if (!text) return null;101  CITY_STATE_RE.lastIndex = 0;102  for (const m of text.matchAll(CITY_STATE_RE)) {103    // "54MW Data Center In Dallas, Texas": drop leading words that cannot start a city name until a proper noun is left104    const words = m[1]!.replace(/\.$/, "").split(/[ ]/);105    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();106    const city = words.join(" ");107    const st = m[2]!;108    const stateInfo = STATE_NAMES.get(st.length <= 2 ? st : st.toLowerCase());109    if (!stateInfo || NOT_A_CITY.test(city)) continue;110    // "County, Virginia" is fine, but "Virginia, USA"-style matches are not a city111    if (STATE_NAMES.has(city.toLowerCase())) continue;112    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 };113  }114  // earliest hit wins, but a proper city beats a county / parish named earlier in the same text115  let best: { entry: CityEntry; index: number; county: boolean } | null = null;116  for (const { entry, re } of cityPatterns) {117    const m = re.exec(text);118    if (!m) continue;119    const county = /\b(County|Parish|Township)$/.test(entry.city);120    if (!best || (county === best.county ? m.index < best.index : !county)) best = { entry, index: m.index, county };121  }122  if (best) return { city: best.entry.city, region: best.entry.region ?? null, country: best.entry.country, method: "lexicon:city", index: best.index };123  STATE_RE.lastIndex = 0;124  const sm = STATE_RE.exec(text);125  if (sm) {126    const info = STATE_NAMES.get(sm[1]!.toLowerCase());127    // "Georgia" alone is the US state unless the text is clearly about the country128    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 };129  }130  const country = countryFromTextSafe(text);131  if (country) return { city: null, region: null, country, method: "lexicon:country", index: 0 };132  return null;133}134135/** countryFromText with the traps removed: "Latin/North America" is not the US, "Georgia" is a US state, "Jordan" is a person. */136export function countryFromTextSafe(text: string | null | undefined): string | null {137  if (!text) return null;138  const cleaned = text139    .replace(/\b(Latin|North|South|Central) America(n)?\b/g, " ")140    .replace(/\bAmericas\b/g, " ")141    .replace(/\bGeorgia\b/g, (m) => (/\b(Tbilisi|Republic of Georgia|Caucasus)\b/.test(text) ? m : " "))142    .replace(/\b[A-Z][a-z]+ Jordan\b/g, " ");143  return countryFromText(cleaned);144}145