spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { EventType, PageType } from "@dci/core";2import { inferStatus } from "@dci/core";3import { projectMwFigures } from "./mw-figures.js";45/**6 * Deterministic page classification. URL rules first (a facility URL wins over whatever the body says — a facility7 * page mentioning "acquired in 2019" stays a facility page), then title/body keyword rules, first match wins.8 * An LLM may be plugged in later as an assistive classifier for "unknown" only.9 */1011/**12 * Data-center vocabulary in the languages of the markets we index: English, German (Rechenzentrum/-zentren),13 * French (centre de données), Spanish (centro de datos), Portuguese (centro de dados), Dutch (datacentrum/-centra),14 * Nordic (datacenter, datasenter, datacentral, serverhall). Used by the relevance gate and the text rules.15 */16export const DC_WORD_SRC = "data ?cent(?:er|re)s?|rechenzentr(?:um|en|ums)|centres? de donn[ée]es|centros? de datos|centros? de dados|datacentr(?:um|a)|datacenters?|datasent(?:er|re)|datacentral(?:er)?|serverhal(?:l|len|lar)";17const DC_WORD_RE = new RegExp(`\\b(?:${DC_WORD_SRC})\\b`, "i");1819const URL_RULES: Array<[RegExp, PageType]> = [20 [/\/(data-?cent(er|re)s?|facilities|locations|colocation|ibx|campus(es)?|rechenzentr(um|en)|datacenters?|centres?-de-donnees|centros?-de-datos)\/[a-z0-9-]+\/?$/i, "facility_page"],21 [/\/(data-?cent(er|re)s?|facilities|locations|colocation|rechenzentr(um|en)|datacenters?)\/?$/i, "facility_index"],22 [/\/(news(room)?|press(-releases?)?|media|announcements|blog|presse|actualites|noticias)\/?$/i, "news_index"],23 [/\/(news|press|media|announcements|newsroom|presse|actualites|noticias)\/./i, "press_release"],24 [/\/(regions?|global-infrastructure|locations|about\/locations|infrastructure\/regions)/i, "cloud_region"],25 [/sitemap.*\.xml/i, "sitemap"],26 [/\/(investors?|ir|financials?|sec-filings|annual-report|10-?k|quarterly)/i, "financial_disclosure"],27 [/\/(planning|permits?|zoning|development-applications?|applications?)/i, "planning_document"],28];2930/** A PDF is a planning document only when its URL or title says so; other PDFs stay `unknown` for the news parser. */31export const PDF_URL_RE = /\.pdf(\?|$)/i;32export const PLANNING_VOCAB_RE = /\b(planning|permit(s|ting)?|zoning|rezon\w*|application|environmental|eia|site[- ]plan|council|committee|agenda|bebauungsplan|plan local d'urbanisme|permis de construire|licencia de obras)\b/i;3334const TEXT_RULES: Array<[RegExp, PageType]> = [35 [/\b(acquires|acquisition of|to acquire|completes acquisition|agreed to acquire|acquired|übernimmt|übernahme|rachète|acquisition de|adquiere|adquisición)\b/i, "acquisition"],36 [/\b(expansion|expands|expanding|adds? \d+ ?MW|additional (capacity|phase)|new phase|erweitert|erweiterung|agrandit|agrandissement|ampliación|amplía|uitbreiding)\b/i, "expansion"],37 [/\b(closes?|closure|closing|decommission|shut(ting)? down|exit(s|ing)? (the )?market)\b/i, "closure"],38 [/\b(planning (application|permission|commission|board)|rezon(e|ing)|site plan|conditional use|environmental (assessment|impact)|permit(ting)?|bebauungsplan|permis de construire)\b/i, "planning_document"],39 [/\b(under construction|breaks? ground|groundbreaking|topping out|construction (update|progress|milestone)|shell (is )?complete|spatenstich|im bau|en construction|en construcción|em construção|in aanbouw)\b/i, "construction_update"],40 [/\b(substation|transmission line|power (purchase|supply) agreement|PPA|grid connection|interconnection agreement|utility|megawatts of power|gigawatt|nuclear|SMR|gas turbine|solar farm|wind farm|umspannwerk|netzanschluss|raccordement au réseau)\b/i, "power_infrastructure"],41 [/\b(cloud region|availability zones?|launches? (a )?new region|region (now )?(available|generally available|GA)|sovereign cloud)\b/i, "cloud_region"],42 [new RegExp(`\\b(announces?|unveils?|plans (to build|for)|to build|will build|new (${DC_WORD_SRC}|campus|facility)|neues? (${DC_WORD_SRC})|nouveau (${DC_WORD_SRC})|nuevo (${DC_WORD_SRC})|novo (${DC_WORD_SRC})|nieuw (${DC_WORD_SRC})|investment of|to invest|investiert|investit|invierte|\\$\\d+(\\.\\d+)? ?(billion|million)|€\\d+|£\\d+)\\b`, "i"), "project_announcement"],43 [/\b(press release|for immediate release|newsroom|media contact|pressemitteilung|communiqué de presse|nota de prensa|comunicado de imprensa|persbericht)\b/i, "press_release"],44 [/\b(colocation|carrier-neutral|interconnection|meet-me room|rack|cabinet|cage|cross[- ]connect|N\+1|2N|redundan|PUE|Tier (III|IV|3|4)|uptime institute)\b/i, "facility_page"],45];4647export interface Classification { pageType: PageType; rule: string; eventType?: EventType; mw: number[]; status?: string | null }4849export function classifyPage(url: string, title: string | null, text: string): Classification {50 if (PDF_URL_RE.test(url)) {51 const isPlanning = PLANNING_VOCAB_RE.test(url) || (title != null && PLANNING_VOCAB_RE.test(title));52 return finish(isPlanning ? "planning_document" : "unknown", isPlanning ? "url:pdf+planning" : "url:pdf", title, text);53 }54 for (const [re, pt] of URL_RULES) if (re.test(url)) return finish(pt, `url:${re.source.slice(0, 30)}`, title, text);55 const head = `${title ?? ""}\n${text.slice(0, 4000)}`;56 for (const [re, pt] of TEXT_RULES) if (re.test(head)) return finish(pt, `text:${re.source.slice(0, 30)}`, title, text);57 return finish("unknown", "none", title, text);58}5960function finish(pageType: PageType, rule: string, title: string | null, text: string): Classification {61 const head = `${title ?? ""}\n${text.slice(0, 6000)}`;62 // site figures only: "brings the portfolio to 1.4 GW" is not a capacity of this page's subject63 const mw = projectMwFigures(head);64 const status = inferStatus(head);65 const eventType = eventTypeFor(pageType, head);66 return { pageType, rule, eventType, mw, status };67}6869export function eventTypeFor(pageType: PageType, text: string): EventType | undefined {70 switch (pageType) {71 case "acquisition": return "acquisition";72 case "expansion": return "expansion_announced";73 case "closure": return "closure";74 case "construction_update": return /\b(breaks? ground|groundbreaking|construction (has )?(started|begun|commenced))\b/i.test(text) ? "construction_started" : "project_status_changed";75 case "planning_document": return /\b(approved|approval granted|granted permission)\b/i.test(text) ? "planning_approved" : "planning_filed";76 case "power_infrastructure": return "power_agreement";77 case "cloud_region": return /\b(announc|plans?|upcoming|coming)/i.test(text) && !/\b(now available|generally available|launched|is live|opens)\b/i.test(text) ? "cloud_region_announced" : "cloud_region_launched";78 case "project_announcement": return /\b(invest(ment|s)? of|to invest|\$\d)/i.test(text) && !new RegExp(`\\b(${DC_WORD_SRC}|campus|MW)\\b`, "i").test(text) ? "investment_announced" : "project_announced";79 case "press_release": return "news";80 case "facility_page": return "facility_updated";81 default: return undefined;82 }83}8485/** Is this text about data centers at all (any of the indexed languages)? Used to drop irrelevant newsroom items early. */86export function isDataCenterRelevant(text: string): boolean {87 return DC_WORD_RE.test(text) || /\b(colocation|hyperscale|cloud region|availability zone|MW|megawatt|campus|interconnection|IBX|server farm|AI infrastructure|GPU cluster|compute capacity|digital infrastructure|colocación|colocatie|hyperscaler)\b/i.test(text);88}89