spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1/**2 * ClinicalTrials.gov country names → ISO 3166-1 alpha-3 (pure, no I/O).3 *4 * ClinicalTrials.gov stores the country of a location as free text chosen by the registrant from5 * its own list; the API v2 returns mostly short English names ("United States", "South Korea",6 * "Turkey (Türkiye)", "Reunion") while older records and the legacy API used ISO-style long names7 * ("Korea, Republic of", "Russian Federation", "Réunion", "Viet Nam"). Both families are accepted8 * here. Territories keep their own ISO code (Puerto Rico → PRI, Hong Kong → HKG, Réunion → REU…)9 * because that is how the registrant counted the site; the UI may roll them up if it wishes.10 *11 * Dissolved states that have no ISO 3166-1 code today are deliberately left unmapped and listed in12 * `UNMAPPED_COUNTRY_NAMES` so a test fails when a new name appears in the database without a decision.13 */1415export interface CountryCode {16 /** ISO 3166-1 alpha-3 (Kosovo uses the user-assigned XKX, as the World Bank and EU do). */17 iso3: string;18 /** Display name (short English form). */19 name: string;20 /** Spellings seen in ClinicalTrials.gov exports, in addition to `name`. */21 aliases?: string[];22}2324export const COUNTRY_CODES: readonly CountryCode[] = [25 { iso3: 'AFG', name: 'Afghanistan' },26 { iso3: 'ALB', name: 'Albania' },27 { iso3: 'DZA', name: 'Algeria' },28 { iso3: 'ASM', name: 'American Samoa' },29 { iso3: 'AND', name: 'Andorra' },30 { iso3: 'AGO', name: 'Angola' },31 { iso3: 'ATG', name: 'Antigua and Barbuda' },32 { iso3: 'ARG', name: 'Argentina' },33 { iso3: 'ARM', name: 'Armenia' },34 { iso3: 'ABW', name: 'Aruba' },35 { iso3: 'AUS', name: 'Australia' },36 { iso3: 'AUT', name: 'Austria' },37 { iso3: 'AZE', name: 'Azerbaijan' },38 { iso3: 'BHS', name: 'Bahamas', aliases: ['The Bahamas', 'Bahamas, The'] },39 { iso3: 'BHR', name: 'Bahrain' },40 { iso3: 'BGD', name: 'Bangladesh' },41 { iso3: 'BRB', name: 'Barbados' },42 { iso3: 'BLR', name: 'Belarus' },43 { iso3: 'BEL', name: 'Belgium' },44 { iso3: 'BLZ', name: 'Belize' },45 { iso3: 'BEN', name: 'Benin' },46 { iso3: 'BMU', name: 'Bermuda' },47 { iso3: 'BTN', name: 'Bhutan' },48 { iso3: 'BOL', name: 'Bolivia', aliases: ['Bolivia, Plurinational State of', 'Bolivia (Plurinational State of)'] },49 { iso3: 'BIH', name: 'Bosnia and Herzegovina' },50 { iso3: 'BWA', name: 'Botswana' },51 { iso3: 'BRA', name: 'Brazil' },52 { iso3: 'BRN', name: 'Brunei', aliases: ['Brunei Darussalam'] },53 { iso3: 'BGR', name: 'Bulgaria' },54 { iso3: 'BFA', name: 'Burkina Faso' },55 { iso3: 'BDI', name: 'Burundi' },56 { iso3: 'KHM', name: 'Cambodia' },57 { iso3: 'CMR', name: 'Cameroon' },58 { iso3: 'CAN', name: 'Canada' },59 { iso3: 'CPV', name: 'Cape Verde', aliases: ['Cabo Verde'] },60 { iso3: 'CYM', name: 'Cayman Islands' },61 { iso3: 'CAF', name: 'Central African Republic' },62 { iso3: 'TCD', name: 'Chad' },63 { iso3: 'CHL', name: 'Chile' },64 { iso3: 'CHN', name: 'China' },65 { iso3: 'COL', name: 'Colombia' },66 { iso3: 'COM', name: 'Comoros' },67 { iso3: 'COG', name: 'Congo', aliases: ['Republic of the Congo', 'Congo, Republic of the'] },68 { iso3: 'COD', name: 'Democratic Republic of the Congo', aliases: ['Congo, The Democratic Republic of the', 'Congo, Democratic Republic of the', 'Congo, The Democratic Republic'] },69 { iso3: 'CRI', name: 'Costa Rica' },70 { iso3: 'CIV', name: "Côte d'Ivoire", aliases: ["Cote d'Ivoire", 'Ivory Coast'] },71 { iso3: 'HRV', name: 'Croatia' },72 { iso3: 'CUB', name: 'Cuba' },73 { iso3: 'CUW', name: 'Curaçao', aliases: ['Curacao'] },74 { iso3: 'CYP', name: 'Cyprus' },75 { iso3: 'CZE', name: 'Czechia', aliases: ['Czech Republic'] },76 { iso3: 'DNK', name: 'Denmark' },77 { iso3: 'DJI', name: 'Djibouti' },78 { iso3: 'DMA', name: 'Dominica' },79 { iso3: 'DOM', name: 'Dominican Republic' },80 { iso3: 'ECU', name: 'Ecuador' },81 { iso3: 'EGY', name: 'Egypt' },82 { iso3: 'SLV', name: 'El Salvador' },83 { iso3: 'GNQ', name: 'Equatorial Guinea' },84 { iso3: 'ERI', name: 'Eritrea' },85 { iso3: 'EST', name: 'Estonia' },86 { iso3: 'SWZ', name: 'Eswatini', aliases: ['Swaziland'] },87 { iso3: 'ETH', name: 'Ethiopia' },88 { iso3: 'FRO', name: 'Faroe Islands', aliases: ['Faeroe Islands'] },89 { iso3: 'FJI', name: 'Fiji' },90 { iso3: 'FIN', name: 'Finland' },91 { iso3: 'FRA', name: 'France' },92 { iso3: 'GUF', name: 'French Guiana' },93 { iso3: 'PYF', name: 'French Polynesia' },94 { iso3: 'GAB', name: 'Gabon' },95 { iso3: 'GMB', name: 'Gambia', aliases: ['The Gambia', 'Gambia, The'] },96 { iso3: 'GEO', name: 'Georgia' },97 { iso3: 'DEU', name: 'Germany' },98 { iso3: 'GHA', name: 'Ghana' },99 { iso3: 'GIB', name: 'Gibraltar' },100 { iso3: 'GRC', name: 'Greece' },101 { iso3: 'GRL', name: 'Greenland' },102 { iso3: 'GRD', name: 'Grenada' },103 { iso3: 'GLP', name: 'Guadeloupe' },104 { iso3: 'GUM', name: 'Guam' },105 { iso3: 'GTM', name: 'Guatemala' },106 { iso3: 'GIN', name: 'Guinea' },107 { iso3: 'GNB', name: 'Guinea-Bissau' },108 { iso3: 'GUY', name: 'Guyana' },109 { iso3: 'HTI', name: 'Haiti' },110 { iso3: 'HND', name: 'Honduras' },111 { iso3: 'HKG', name: 'Hong Kong', aliases: ['Hong Kong SAR', 'Hong Kong, China'] },112 { iso3: 'HUN', name: 'Hungary' },113 { iso3: 'ISL', name: 'Iceland' },114 { iso3: 'IND', name: 'India' },115 { iso3: 'IDN', name: 'Indonesia' },116 { iso3: 'IRN', name: 'Iran', aliases: ['Iran, Islamic Republic of', 'Iran (Islamic Republic of)'] },117 { iso3: 'IRQ', name: 'Iraq' },118 { iso3: 'IRL', name: 'Ireland' },119 { iso3: 'ISR', name: 'Israel' },120 { iso3: 'ITA', name: 'Italy' },121 { iso3: 'JAM', name: 'Jamaica' },122 { iso3: 'JPN', name: 'Japan' },123 { iso3: 'JOR', name: 'Jordan' },124 { iso3: 'KAZ', name: 'Kazakhstan' },125 { iso3: 'KEN', name: 'Kenya' },126 { iso3: 'PRK', name: 'North Korea', aliases: ["Korea, Democratic People's Republic of", "Democratic People's Republic of Korea"] },127 { iso3: 'KOR', name: 'South Korea', aliases: ['Korea, Republic of', 'Republic of Korea', 'Korea'] },128 { iso3: 'XKX', name: 'Kosovo' },129 { iso3: 'KWT', name: 'Kuwait' },130 { iso3: 'KGZ', name: 'Kyrgyzstan' },131 { iso3: 'LAO', name: 'Laos', aliases: ["Lao People's Democratic Republic"] },132 { iso3: 'LVA', name: 'Latvia' },133 { iso3: 'LBN', name: 'Lebanon' },134 { iso3: 'LSO', name: 'Lesotho' },135 { iso3: 'LBR', name: 'Liberia' },136 { iso3: 'LBY', name: 'Libya', aliases: ['Libyan Arab Jamahiriya'] },137 { iso3: 'LIE', name: 'Liechtenstein' },138 { iso3: 'LTU', name: 'Lithuania' },139 { iso3: 'LUX', name: 'Luxembourg' },140 { iso3: 'MAC', name: 'Macao', aliases: ['Macau', 'Macao SAR'] },141 { iso3: 'MDG', name: 'Madagascar' },142 { iso3: 'MWI', name: 'Malawi' },143 { iso3: 'MYS', name: 'Malaysia' },144 { iso3: 'MDV', name: 'Maldives' },145 { iso3: 'MLI', name: 'Mali' },146 { iso3: 'MLT', name: 'Malta' },147 { iso3: 'MTQ', name: 'Martinique' },148 { iso3: 'MRT', name: 'Mauritania' },149 { iso3: 'MUS', name: 'Mauritius' },150 { iso3: 'MYT', name: 'Mayotte' },151 { iso3: 'MEX', name: 'Mexico' },152 { iso3: 'MDA', name: 'Moldova', aliases: ['Moldova, Republic of', 'Republic of Moldova'] },153 { iso3: 'MCO', name: 'Monaco' },154 { iso3: 'MNG', name: 'Mongolia' },155 { iso3: 'MNE', name: 'Montenegro' },156 { iso3: 'MAR', name: 'Morocco' },157 { iso3: 'MOZ', name: 'Mozambique' },158 { iso3: 'MMR', name: 'Myanmar', aliases: ['Burma'] },159 { iso3: 'NAM', name: 'Namibia' },160 { iso3: 'NPL', name: 'Nepal' },161 { iso3: 'NLD', name: 'Netherlands', aliases: ['The Netherlands', 'Netherlands, The'] },162 { iso3: 'NCL', name: 'New Caledonia' },163 { iso3: 'NZL', name: 'New Zealand' },164 { iso3: 'NIC', name: 'Nicaragua' },165 { iso3: 'NER', name: 'Niger' },166 { iso3: 'NGA', name: 'Nigeria' },167 { iso3: 'MKD', name: 'North Macedonia', aliases: ['Macedonia, The Former Yugoslav Republic of', 'Macedonia', 'North Macedonia, Republic of'] },168 { iso3: 'MNP', name: 'Northern Mariana Islands' },169 { iso3: 'NOR', name: 'Norway' },170 { iso3: 'OMN', name: 'Oman' },171 { iso3: 'PAK', name: 'Pakistan' },172 { iso3: 'PSE', name: 'Palestinian Territories', aliases: ['Palestinian Territory, occupied', 'Palestinian Territory', 'Palestine, State of', 'State of Palestine', 'Palestine'] },173 { iso3: 'PAN', name: 'Panama' },174 { iso3: 'PNG', name: 'Papua New Guinea' },175 { iso3: 'PRY', name: 'Paraguay' },176 { iso3: 'PER', name: 'Peru' },177 { iso3: 'PHL', name: 'Philippines' },178 { iso3: 'POL', name: 'Poland' },179 { iso3: 'PRT', name: 'Portugal' },180 { iso3: 'PRI', name: 'Puerto Rico' },181 { iso3: 'QAT', name: 'Qatar' },182 { iso3: 'REU', name: 'Réunion', aliases: ['Reunion'] },183 { iso3: 'ROU', name: 'Romania' },184 { iso3: 'RUS', name: 'Russia', aliases: ['Russian Federation'] },185 { iso3: 'RWA', name: 'Rwanda' },186 { iso3: 'KNA', name: 'Saint Kitts and Nevis' },187 { iso3: 'LCA', name: 'Saint Lucia' },188 { iso3: 'VCT', name: 'Saint Vincent and the Grenadines' },189 { iso3: 'WSM', name: 'Samoa' },190 { iso3: 'SMR', name: 'San Marino' },191 { iso3: 'STP', name: 'São Tomé and Príncipe', aliases: ['Sao Tome and Principe'] },192 { iso3: 'SAU', name: 'Saudi Arabia' },193 { iso3: 'SEN', name: 'Senegal' },194 { iso3: 'SRB', name: 'Serbia' },195 { iso3: 'SYC', name: 'Seychelles' },196 { iso3: 'SLE', name: 'Sierra Leone' },197 { iso3: 'SGP', name: 'Singapore' },198 { iso3: 'SVK', name: 'Slovakia' },199 { iso3: 'SVN', name: 'Slovenia' },200 { iso3: 'SLB', name: 'Solomon Islands' },201 { iso3: 'SOM', name: 'Somalia' },202 { iso3: 'ZAF', name: 'South Africa' },203 { iso3: 'SSD', name: 'South Sudan' },204 { iso3: 'ESP', name: 'Spain' },205 { iso3: 'LKA', name: 'Sri Lanka' },206 { iso3: 'SDN', name: 'Sudan' },207 { iso3: 'SUR', name: 'Suriname' },208 { iso3: 'SWE', name: 'Sweden' },209 { iso3: 'CHE', name: 'Switzerland' },210 { iso3: 'SYR', name: 'Syria', aliases: ['Syrian Arab Republic'] },211 { iso3: 'TWN', name: 'Taiwan', aliases: ['Taiwan, Province of China'] },212 { iso3: 'TJK', name: 'Tajikistan' },213 { iso3: 'TZA', name: 'Tanzania', aliases: ['Tanzania, United Republic of', 'United Republic of Tanzania'] },214 { iso3: 'THA', name: 'Thailand' },215 { iso3: 'TLS', name: 'Timor-Leste', aliases: ['East Timor'] },216 { iso3: 'TGO', name: 'Togo' },217 { iso3: 'TON', name: 'Tonga' },218 { iso3: 'TTO', name: 'Trinidad and Tobago' },219 { iso3: 'TUN', name: 'Tunisia' },220 { iso3: 'TUR', name: 'Türkiye', aliases: ['Turkey (Türkiye)', 'Turkey', 'Turkiye'] },221 { iso3: 'TKM', name: 'Turkmenistan' },222 { iso3: 'UGA', name: 'Uganda' },223 { iso3: 'UKR', name: 'Ukraine' },224 { iso3: 'ARE', name: 'United Arab Emirates' },225 { iso3: 'GBR', name: 'United Kingdom', aliases: ['United Kingdom of Great Britain and Northern Ireland', 'UK', 'Great Britain'] },226 { iso3: 'USA', name: 'United States', aliases: ['United States of America', 'USA'] },227 { iso3: 'URY', name: 'Uruguay' },228 { iso3: 'UZB', name: 'Uzbekistan' },229 { iso3: 'VUT', name: 'Vanuatu' },230 { iso3: 'VEN', name: 'Venezuela', aliases: ['Venezuela, Bolivarian Republic of', 'Venezuela (Bolivarian Republic of)'] },231 { iso3: 'VNM', name: 'Vietnam', aliases: ['Viet Nam'] },232 // ClinicalTrials.gov writes "Virgin Islands" for sites in the U.S. Virgin Islands (facilities in St. Thomas / St. Croix); the British Virgin Islands are spelled out.233 { iso3: 'VIR', name: 'U.S. Virgin Islands', aliases: ['Virgin Islands', 'Virgin Islands (U.S.)', 'Virgin Islands, U.S.', 'United States Virgin Islands'] },234 { iso3: 'VGB', name: 'British Virgin Islands', aliases: ['Virgin Islands, British'] },235 { iso3: 'YEM', name: 'Yemen' },236 { iso3: 'ZMB', name: 'Zambia' },237 { iso3: 'ZWE', name: 'Zimbabwe' },238];239240/**241 * Names present in ClinicalTrials.gov exports that have NO current ISO 3166-1 code (dissolved242 * states or an empty field). They keep `iso3 = null` in `trial_site_country_counts`, appear in the243 * table by name, and are never painted on the map.244 */245export const UNMAPPED_COUNTRY_NAMES: readonly string[] = ['', 'Serbia and Montenegro', 'Former Serbia and Montenegro', 'Federal Republic of Yugoslavia', 'Former Yugoslavia', 'Yugoslavia', 'Netherlands Antilles', 'Czechoslovakia', 'Union of Soviet Socialist Republics', 'USSR'];246247/** Lower-case, trimmed, straight apostrophes, collapsed whitespace. */248export function normalizeCountryName(name: string): string {249 return name250 .normalize('NFC')251 .replace(/[‘’ʼ`´]/g, "'")252 .replace(/\s+/g, ' ')253 .trim()254 .toLowerCase();255}256257const NAME_INDEX: ReadonlyMap<string, CountryCode> = (() => {258 const m = new Map<string, CountryCode>();259 for (const c of COUNTRY_CODES) {260 m.set(normalizeCountryName(c.name), c);261 for (const a of c.aliases ?? []) m.set(normalizeCountryName(a), c);262 m.set(c.iso3.toLowerCase(), c);263 }264 return m;265})();266267const ISO_INDEX: ReadonlyMap<string, CountryCode> = new Map(COUNTRY_CODES.map((c) => [c.iso3, c]));268269/** ISO 3166-1 alpha-3 for a ClinicalTrials.gov country name, or null when unknown/unmappable. */270export function countryToIso3(name: string | null | undefined): string | null {271 if (name == null) return null;272 const c = NAME_INDEX.get(normalizeCountryName(name));273 return c ? c.iso3 : null;274}275276/** Full entry (iso3 + display name) for a ClinicalTrials.gov country name or an ISO3 code. */277export function lookupCountry(nameOrIso3: string | null | undefined): CountryCode | null {278 if (nameOrIso3 == null) return null;279 return NAME_INDEX.get(normalizeCountryName(nameOrIso3)) ?? null;280}281282/** Display name for an ISO3 code (falls back to the code). */283export function iso3ToName(iso3: string): string {284 return ISO_INDEX.get(iso3.toUpperCase())?.name ?? iso3;285}286287export function isKnownUnmapped(name: string): boolean {288 const n = normalizeCountryName(name);289 return UNMAPPED_COUNTRY_NAMES.some((u) => normalizeCountryName(u) === n);290}291