/** * Conservative operator inference from a facility name. OpenStreetMap / Wikidata features often carry no * `operator` tag while the name starts with a canonical brand ("Equinix AM3", "Google Data Center, Elmina", * "Meta Odense Data Center"). Only the curated canonical table (names + aliases) is used, matched as a whole-token * prefix of the name — never fuzzy, never inside the name. Provenance method: `inferred:name-prefix`. */ import { normalizeName } from "@dci/core"; import { CANONICAL_OPERATORS, type CanonicalOperator } from "./canonical-operators.js"; export const OPERATOR_INFERENCE_METHOD = "inferred:name-prefix"; /** * Brand words that are also ordinary words or too short to stand alone ("Switch", "Meta", "Apple", "IO", "Spark"): * accepted only when the rest of the name looks like a facility (data-center vocabulary or a facility code). */ /** Ambiguous brands where data-center vocabulary in the raw name is enough ("Meta Odense Data Center", "Apple Data Center Viborg"). */ const AMBIGUOUS_VOCAB: ReadonlySet = new Set(["meta", "apple", "switch", "aligned", "vantage", "compass", "lambda", "verne", "moro"]); /** Ambiguous brands that additionally need a facility code ("Vantage WA11", "Switch Las Vegas 8"). */ const AMBIGUOUS_CODE: ReadonlySet = new Set(["stack", "stc", "esr", "kio", "gds", "cdc", "adc", "att", "mci", "cht", "telx", "skt", "ctg", "hkt", "pdg", "gdh", "ngd", "dft", "dlr", "oci", "gcp", "aws", "ovh", "ntt", "amt", "apld", "bce", "yti", "ytl", "udcs", "jio", "sify", "inap"]); /** Ordinary words: never inferred from a single token whatever follows ("Green Data Center Norway", "Orange County"). */ const NEVER_ALONE: ReadonlySet = new Set(["green", "constant", "beyond", "stream", "bulk", "chief", "spark", "orange", "bell", "tract", "packet", "io", "tm", "kt", "bt", "tel", "tata", "reliance", "iliad", "scala", "elea", "echelon", "rowan", "takoda", "novva", "revera", "sabey", "ark", "virtus", "cirion", "nxtra", "khazna", "linode", "lumen"]); const PLACE_WORDS = /^(county|parish|township|city|town|village|valley|street|st|road|rd|avenue|ave|park|lake|river|island|bay|beach|hills?|mountain|falls|creek)$/; const FACILITY_HINT_RE = /\b(data ?cent(er|re)s?|datacent(er|re)s?|rechenzentrum|centre de donn[ée]es|centro de datos|campus|colocation|colo|dc|idc|hub|pop|site)\b/i; const CODE_HINT_RE = /(^|[^A-Za-z0-9])[A-Z]{1,6}-?\d{1,3}[A-Z]?([^A-Za-z0-9]|$)|\b[A-Z][a-z]+ \d{1,2}\b/; interface BrandEntry { tokens: string[]; op: CanonicalOperator; canonical: boolean } let brands: BrandEntry[] | null = null; function table(): BrandEntry[] { if (brands) return brands; const out: BrandEntry[] = []; for (const op of CANONICAL_OPERATORS) { const seen = new Set(); for (const [i, raw] of [op.name, ...op.aliases].entries()) { const tokens = normalizeName(raw).split(" ").filter(Boolean); const key = tokens.join(" "); if (!tokens.length || seen.has(key)) continue; // a single token shorter than 3 characters ("IO", "TM") is never a safe prefix if (tokens.length === 1 && tokens[0]!.length < 3) continue; seen.add(key); out.push({ tokens, op, canonical: i === 0 }); } } // longest brand first so "Digital Realty" wins over "Digital"-prefixed shorter aliases out.sort((a, b) => b.tokens.length - a.tokens.length || b.tokens.join(" ").length - a.tokens.join(" ").length); brands = out; return out; } export interface InferredOperator { name: string; matched: string; method: string } /** * Operator implied by the name's leading tokens, or null. Rules: * - the brand tokens must be the first tokens of the normalized name (noise words like "data center" already removed); * - the name must carry something beyond the brand (a site / code / vocabulary) unless it equals the canonical name exactly; * - ambiguous brands require data-center vocabulary or a facility code in the raw name. */ export function inferOperatorFromName(name: string | null | undefined): InferredOperator | null { if (!name) return null; const tokens = normalizeName(name).split(" ").filter(Boolean); if (!tokens.length) return null; for (const b of table()) { if (b.tokens.length > tokens.length) continue; if (!b.tokens.every((t, i) => tokens[i] === t)) continue; const rest = tokens.slice(b.tokens.length); const brandKey = b.tokens.join(" "); const nameOnly = rest.length === 0; if (nameOnly && !b.canonical) continue; // "AWS" alone is not a facility name if (b.tokens.length === 1) { if (NEVER_ALONE.has(brandKey)) continue; if (AMBIGUOUS_CODE.has(brandKey) && !CODE_HINT_RE.test(name)) continue; if (AMBIGUOUS_VOCAB.has(brandKey) && !(FACILITY_HINT_RE.test(name) || CODE_HINT_RE.test(name))) continue; } // the rest must look like a site, not a place name or a different business ("Orange County", "Apple Store") if (rest.length && (PLACE_WORDS.test(rest[0]!) || /^(store|shop|office|offices|headquarters|hq|fiber|retail|bank|insurance)\b/.test(rest.join(" ")))) continue; return { name: b.op.name, matched: brandKey, method: OPERATOR_INFERENCE_METHOD }; } return null; }