diff --git a/package-lock.json b/package-lock.json index 9e5ee5e..25215aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@js-temporal/polyfill": "^0.4.4", "@observablehq/framework": "^1.9.0", "@vvo/tzdb": "^6.141.0", + "all-the-cities": "^3.1.0", "d3-dsv": "^3.0.1", "d3-time-format": "^4.1.0", "diacritics": "^1.3.0", @@ -23,6 +24,7 @@ "@dotenvx/dotenvx": "^1.5.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@tsconfig/node20": "^20.1.4", + "@types/all-the-cities": "^3.1.3", "@types/diacritics": "^1.3.3", "@types/node": "^20.14.9", "@typescript-eslint/eslint-plugin": "^7.14.1", @@ -359,7 +361,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -2023,6 +2024,12 @@ "integrity": "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==", "dev": true }, + "node_modules/@types/all-the-cities": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/all-the-cities/-/all-the-cities-3.1.3.tgz", + "integrity": "sha512-nmOKDsZi6ANeGto8P4E2Zkk4q/oLN94Jd0++zS0NLaGOUcCvIqNEOKn9btF2xDknaKicCpQymg0U9Llfz+Ourw==", + "dev": true + }, "node_modules/@types/diacritics": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/diacritics/-/diacritics-1.3.3.tgz", @@ -2482,6 +2489,14 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/all-the-cities": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/all-the-cities/-/all-the-cities-3.1.0.tgz", + "integrity": "sha512-Jx+mZR7lUVSX+qk785T3MRkAJYMvUpOmeNQSCbUl1WcCNpBUKJsQJ6uZzOl3/bWXVIUx1UhDFs8ASDHSI3rvzg==", + "dependencies": { + "pbf": "^3.2.1" + } + }, "node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -4909,6 +4924,25 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -6221,6 +6255,18 @@ "node": ">= 14.16" } }, + "node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -6453,6 +6499,11 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -6611,6 +6662,14 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", diff --git a/package.json b/package.json index b684d5a..f6e817a 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@js-temporal/polyfill": "^0.4.4", "@observablehq/framework": "^1.9.0", "@vvo/tzdb": "^6.141.0", + "all-the-cities": "^3.1.0", "d3-dsv": "^3.0.1", "d3-time-format": "^4.1.0", "diacritics": "^1.3.0", @@ -20,6 +21,7 @@ "@dotenvx/dotenvx": "^1.5.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@tsconfig/node20": "^20.1.4", + "@types/all-the-cities": "^3.1.3", "@types/diacritics": "^1.3.3", "@types/node": "^20.14.9", "@typescript-eslint/eslint-plugin": "^7.14.1", diff --git a/src/lib/Iso3166.ts b/src/lib/Iso3166.ts index 9317d7d..4273dda 100644 --- a/src/lib/Iso3166.ts +++ b/src/lib/Iso3166.ts @@ -1,5 +1,6 @@ import { Schema } from '@effect/schema' import { rawTimeZones } from '@vvo/tzdb' +import cities from 'all-the-cities' import diacritics from 'diacritics' import { Array, Option, type Predicate, String, flow } from 'effect' import iso3166 from 'i18n-iso-countries' @@ -12,6 +13,8 @@ export const isAlpha2Code: Predicate.Refinement = (u): u is export const Alpha2CodeSchema: Schema.Schema = Schema.String.pipe(Schema.filter(isAlpha2Code)) +const cities10000 = Array.filter(cities, city => city.population >= 10_000) + export const guessCountry: (location: string) => Option.Option = flow( String.replaceAll('.', ''), String.replaceAll(/\((.+?)\)/g, ', $1'), @@ -40,6 +43,15 @@ export const guessCountry: (location: string) => Option.Option = flo timeZone => timeZone.countryCode, ), ), + Option.orElse(() => + Option.map( + Array.findFirst( + cities10000, + city => diacritics.remove(location).toLowerCase() === diacritics.remove(city.name).toLowerCase(), + ), + city => city.country, + ), + ), ), ), Option.filter(isAlpha2Code), diff --git a/test/Iso3166.test.ts b/test/Iso3166.test.ts index 6a76464..174b5ec 100644 --- a/test/Iso3166.test.ts +++ b/test/Iso3166.test.ts @@ -11,12 +11,14 @@ describe('guessCountry', () => { ['Colorado - United States', 'US'], ['Czech Republic', 'CZ'], ['Czechia', 'CZ'], + ['Düsseldorf', 'DE'], ['GHANA', 'GH'], ['London', 'GB'], ['London, Ontario', 'CA'], ['London, UK', 'GB'], ['London, United Kingdom', 'GB'], ['New York City and Los Angeles', 'US'], + ['Norwich', 'GB'], ['Québec', 'CA'], ['Rio de Janeiro, Brazil.', 'BR'], ['Sunnyvale, California', 'US'], @@ -31,18 +33,12 @@ describe('guessCountry', () => { expect(actual).toStrictEqual(Option.some(expected)) }) - test.for([ - 'Babol iran', - 'Düsseldorf', - 'Fayetteville GA (near Atlanta)', - 'Mars', - 'Norwich', - 'Southeast Asia', - 'The UK', - 'Yale university', - ])("doesn't guess %s", input => { - const actual = _.guessCountry(input) + test.for(['Babol iran', 'Fayetteville GA (near Atlanta)', 'Mars', 'Southeast Asia', 'The UK', 'Yale university'])( + "doesn't guess %s", + input => { + const actual = _.guessCountry(input) - expect(actual).toStrictEqual(Option.none()) - }) + expect(actual).toStrictEqual(Option.none()) + }, + ) })