diff --git a/.github/workflows/refresh-sources.yml b/.github/workflows/refresh-sources.yml new file mode 100644 index 0000000..6890fea --- /dev/null +++ b/.github/workflows/refresh-sources.yml @@ -0,0 +1,41 @@ +name: Refresh sources + +on: + # The day before the weekly build, so a newer source is published on Monday. + schedule: + - cron: "0 4 * * 0" + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: refresh-sources + cancel-in-progress: false + +jobs: + refresh: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + # Never fails: a source that can't be read keeps its saved copy until next week. + - run: npm run refresh + - name: Check and commit what changed + run: | + if git diff --quiet -- data/reference; then + echo "Nothing changed." + exit 0 + fi + npm run check + npm run build -- tests/fixtures/raw.json --offline + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add data/reference + git commit -m "Refresh saved sources" + git push diff --git a/AGENTS.md b/AGENTS.md index 23dc38c..a37a0e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,3 +13,5 @@ linked, never copied. A new source needs its licence and credit in `src/sources.ts` and the README. - Never correct data here: errors are fixed at the source (Wikidata, GeoNames, Trainline…). +- The saved sources in `data/reference/` (all but `moments.json`) are updated by + `npm run refresh`, never by hand. A city's or a site's id never changes once published. diff --git a/README.md b/README.md index 2b74da5..af483bd 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ built, how many records it has and which apps read it. | | `v1/places/pack.json` | The sights as a Postcards community data pack | | stations | `v1/stations/railways.json` | Open railway stations worldwide, in Postcards' station shape | | stations-europe | `v1/stations-europe/railways.json` | About 70,000 European stations from Trainline, same shape | -| cities | `v1/cities/top.json`, `v1/cities/all.json` | The 10,000 most populous cities, and all 135,000 | +| cities | `v1/cities/top.json`, `v1/cities/all.json` | The 10,000 most populous cities, and all of them (about 135,000) | | regions | `v1/regions/regions.json` | First-level regions (states, provinces, régions) with their names | | airports | `v1/airports/airports.json` | Every airport with an IATA code | | heritage | `v1/heritage/unesco.json` | Every UNESCO World Heritage site, once per country | @@ -45,16 +45,33 @@ an app showing the data shows the source's `attribution` from `sources.json`. | --- | --- | --- | | places, stations | CC0 1.0 | Wikidata | | stations-europe | ODbL 1.0 | [Trainline EU stations](https://github.com/trainline-eu/stations) | -| cities | CC BY 4.0 | [GeoNames](https://www.geonames.org), by way of all-the-cities | +| cities | CC BY 4.0 | [GeoNames](https://www.geonames.org) | | regions | ODbL 1.0 | GeoNames, and [dr5hn countries-states-cities](https://github.com/dr5hn/countries-states-cities-database) for a few names | | airports | ODbL 1.0 | [OpenFlights](https://openflights.org/data.html) (from OurAirports, public domain) | | heritage | CC BY-SA 3.0 IGO | [UNESCO World Heritage Centre](https://whc.unesco.org/en/list/) | | countries | ODbL 1.0 | [mledoze/countries](https://github.com/mledoze/countries), ISO 3166-1 codes | | moments | Each item cites its sources | Written for Postcards (CC0), drawn from Wikidata, GeoNames, UNESCO and Wikivoyage | -The vendored source files are in `data/reference/`: GeoNames' admin-1 names and city codes -(export of 2026-09-30, refreshed with `npm run city-admin1`, see `scripts/city-admin1.ts`), -UNESCO's list (retrieved 2026-07-11) and the moments. +The saved source files are in `data/reference/`: GeoNames' cities and admin-1 names, +UNESCO's list, mledoze/countries and the moments. Airports stay as they are, since +OpenFlights no longer updates its data. + +### Keeping the saved sources current + +`npm run refresh` (weekly, from `.github/workflows/refresh-sources.yml`) looks for a newer +version of each saved source, politely: one conditional request per source, answered "not +modified" when nothing changed (GeoNames' ETag and date, GitHub's content hash, UNESCO's +"modified" date), and GeoNames only once a month since its files change daily. A source is +downloaded only when it changed, and the saved copy is rewritten only when the part used +here changed, compared by SHA-256. `data/reference/sources.lock.json` records what was +seen. + +It never forces anything: a source that can't be reached, answers oddly or would lose more +than 5% of its records keeps its saved copy, and is tried again the next week. Ids never +change: a city GeoNames stops listing is kept (marked `u`) so visits saved against it +still resolve, and UNESCO sites keep their ids in `data/reference/heritage-ids.json` even +when renamed. A refresh is checked like any change (`npm run check` and a build) before it +is committed, and reaches the published data at the next weekly build. - **Wikidata (CC0).** Every name, population, coordinate, category and UNESCO flag comes from Wikidata. CC0 asks for nothing, but every record still names its source: its @@ -73,6 +90,7 @@ npm ci npm run check # typecheck + tests npm run build -- tests/fixtures/raw.json # build from the fixture (add --offline to skip Trainline) npm run fetch -- LU MC # fetch two countries (needs query.wikidata.org) +npm run refresh # look for newer saved sources (add --all for GeoNames mid-month) npm run build ``` diff --git a/data/reference/cities.tsv.gz b/data/reference/cities.tsv.gz new file mode 100644 index 0000000..cc8d8c2 Binary files /dev/null and b/data/reference/cities.tsv.gz differ diff --git a/data/reference/city-admin1.tsv.gz b/data/reference/city-admin1.tsv.gz deleted file mode 100644 index 38f2b29..0000000 Binary files a/data/reference/city-admin1.tsv.gz and /dev/null differ diff --git a/data/reference/countries.json b/data/reference/countries.json new file mode 100644 index 0000000..aba6d27 --- /dev/null +++ b/data/reference/countries.json @@ -0,0 +1 @@ +[{"cca2":"AW","region":"Americas","unMember":false,"name":{"common":"Aruba"},"languages":{"nld":"Dutch","pap":"Papiamento"}},{"cca2":"AF","region":"Asia","unMember":true,"name":{"common":"Afghanistan"},"languages":{"prs":"Dari","pus":"Pashto","tuk":"Turkmen"}},{"cca2":"AO","region":"Africa","unMember":true,"name":{"common":"Angola"},"languages":{"por":"Portuguese"}},{"cca2":"AI","region":"Americas","unMember":false,"name":{"common":"Anguilla"},"languages":{"eng":"English"}},{"cca2":"AX","region":"Europe","unMember":false,"name":{"common":"Åland Islands"},"languages":{"swe":"Swedish"}},{"cca2":"AL","region":"Europe","unMember":true,"name":{"common":"Albania"},"languages":{"sqi":"Albanian"}},{"cca2":"AD","region":"Europe","unMember":true,"name":{"common":"Andorra"},"languages":{"cat":"Catalan"}},{"cca2":"AE","region":"Asia","unMember":true,"name":{"common":"United Arab Emirates"},"languages":{"ara":"Arabic"}},{"cca2":"AR","region":"Americas","unMember":true,"name":{"common":"Argentina"},"languages":{"grn":"Guaraní","spa":"Spanish"}},{"cca2":"AM","region":"Asia","unMember":true,"name":{"common":"Armenia"},"languages":{"hye":"Armenian"}},{"cca2":"AS","region":"Oceania","unMember":false,"name":{"common":"American Samoa"},"languages":{"eng":"English","smo":"Samoan"}},{"cca2":"AQ","region":"Antarctic","unMember":false,"name":{"common":"Antarctica"},"languages":{}},{"cca2":"TF","region":"Antarctic","unMember":false,"name":{"common":"French Southern and Antarctic Lands"},"languages":{"fra":"French"}},{"cca2":"AG","region":"Americas","unMember":true,"name":{"common":"Antigua and Barbuda"},"languages":{"eng":"English"}},{"cca2":"AU","region":"Oceania","unMember":true,"name":{"common":"Australia"},"languages":{"eng":"English"}},{"cca2":"AT","region":"Europe","unMember":true,"name":{"common":"Austria"},"languages":{"bar":"Austro-Bavarian German"}},{"cca2":"AZ","region":"Asia","unMember":true,"name":{"common":"Azerbaijan"},"languages":{"aze":"Azerbaijani","rus":"Russian"}},{"cca2":"BI","region":"Africa","unMember":true,"name":{"common":"Burundi"},"languages":{"fra":"French","run":"Kirundi"}},{"cca2":"BE","region":"Europe","unMember":true,"name":{"common":"Belgium"},"languages":{"deu":"German","fra":"French","nld":"Dutch"}},{"cca2":"BJ","region":"Africa","unMember":true,"name":{"common":"Benin"},"languages":{"fra":"French"}},{"cca2":"BF","region":"Africa","unMember":true,"name":{"common":"Burkina Faso"},"languages":{"fra":"French"}},{"cca2":"BD","region":"Asia","unMember":true,"name":{"common":"Bangladesh"},"languages":{"ben":"Bengali"}},{"cca2":"BG","region":"Europe","unMember":true,"name":{"common":"Bulgaria"},"languages":{"bul":"Bulgarian"}},{"cca2":"BH","region":"Asia","unMember":true,"name":{"common":"Bahrain"},"languages":{"ara":"Arabic"}},{"cca2":"BS","region":"Americas","unMember":true,"name":{"common":"Bahamas"},"languages":{"eng":"English"}},{"cca2":"BA","region":"Europe","unMember":true,"name":{"common":"Bosnia and Herzegovina"},"languages":{"bos":"Bosnian","hrv":"Croatian","srp":"Serbian"}},{"cca2":"BL","region":"Americas","unMember":false,"name":{"common":"Saint Barthélemy"},"languages":{"fra":"French"}},{"cca2":"SH","region":"Africa","unMember":false,"name":{"common":"Saint Helena, Ascension and Tristan da Cunha"},"languages":{"eng":"English"}},{"cca2":"BY","region":"Europe","unMember":true,"name":{"common":"Belarus"},"languages":{"bel":"Belarusian","rus":"Russian"}},{"cca2":"BZ","region":"Americas","unMember":true,"name":{"common":"Belize"},"languages":{"bjz":"Belizean Creole","eng":"English","spa":"Spanish"}},{"cca2":"BM","region":"Americas","unMember":false,"name":{"common":"Bermuda"},"languages":{"eng":"English"}},{"cca2":"BO","region":"Americas","unMember":true,"name":{"common":"Bolivia"},"languages":{"aym":"Aymara","grn":"Guaraní","que":"Quechua","spa":"Spanish"}},{"cca2":"BQ","region":"Americas","unMember":false,"name":{"common":"Caribbean Netherlands"},"languages":{"eng":"English","nld":"Dutch","pap":"Papiamento"}},{"cca2":"BR","region":"Americas","unMember":true,"name":{"common":"Brazil"},"languages":{"por":"Portuguese"}},{"cca2":"BB","region":"Americas","unMember":true,"name":{"common":"Barbados"},"languages":{"eng":"English"}},{"cca2":"BN","region":"Asia","unMember":true,"name":{"common":"Brunei"},"languages":{"msa":"Malay"}},{"cca2":"BT","region":"Asia","unMember":true,"name":{"common":"Bhutan"},"languages":{"dzo":"Dzongkha"}},{"cca2":"BV","region":"Antarctic","unMember":false,"name":{"common":"Bouvet Island"},"languages":{"nor":"Norwegian"}},{"cca2":"BW","region":"Africa","unMember":true,"name":{"common":"Botswana"},"languages":{"eng":"English","tsn":"Tswana"}},{"cca2":"CF","region":"Africa","unMember":true,"name":{"common":"Central African Republic"},"languages":{"fra":"French","sag":"Sango"}},{"cca2":"CA","region":"Americas","unMember":true,"name":{"common":"Canada"},"languages":{"eng":"English","fra":"French"}},{"cca2":"CC","region":"Oceania","unMember":false,"name":{"common":"Cocos (Keeling) Islands"},"languages":{"eng":"English"}},{"cca2":"CH","region":"Europe","unMember":true,"name":{"common":"Switzerland"},"languages":{"fra":"French","gsw":"Swiss German","ita":"Italian","roh":"Romansh"}},{"cca2":"CL","region":"Americas","unMember":true,"name":{"common":"Chile"},"languages":{"spa":"Spanish"}},{"cca2":"CN","region":"Asia","unMember":true,"name":{"common":"China"},"languages":{"zho":"Chinese"}},{"cca2":"CI","region":"Africa","unMember":true,"name":{"common":"Ivory Coast"},"languages":{"fra":"French"}},{"cca2":"CM","region":"Africa","unMember":true,"name":{"common":"Cameroon"},"languages":{"eng":"English","fra":"French"}},{"cca2":"CD","region":"Africa","unMember":true,"name":{"common":"DR Congo"},"languages":{"fra":"French","kon":"Kikongo","lin":"Lingala","lua":"Tshiluba","swa":"Swahili"}},{"cca2":"CG","region":"Africa","unMember":true,"name":{"common":"Republic of the Congo"},"languages":{"fra":"French","kon":"Kikongo","lin":"Lingala"}},{"cca2":"CK","region":"Oceania","unMember":false,"name":{"common":"Cook Islands"},"languages":{"eng":"English","rar":"Cook Islands Māori"}},{"cca2":"CO","region":"Americas","unMember":true,"name":{"common":"Colombia"},"languages":{"spa":"Spanish"}},{"cca2":"KM","region":"Africa","unMember":true,"name":{"common":"Comoros"},"languages":{"ara":"Arabic","fra":"French","zdj":"Comorian"}},{"cca2":"CV","region":"Africa","unMember":true,"name":{"common":"Cape Verde"},"languages":{"por":"Portuguese"}},{"cca2":"CR","region":"Americas","unMember":true,"name":{"common":"Costa Rica"},"languages":{"spa":"Spanish"}},{"cca2":"CU","region":"Americas","unMember":true,"name":{"common":"Cuba"},"languages":{"spa":"Spanish"}},{"cca2":"CW","region":"Americas","unMember":false,"name":{"common":"Curaçao"},"languages":{"eng":"English","nld":"Dutch","pap":"Papiamento"}},{"cca2":"CX","region":"Oceania","unMember":false,"name":{"common":"Christmas Island"},"languages":{"eng":"English"}},{"cca2":"KY","region":"Americas","unMember":false,"name":{"common":"Cayman Islands"},"languages":{"eng":"English"}},{"cca2":"CY","region":"Europe","unMember":true,"name":{"common":"Cyprus"},"languages":{"ell":"Greek","tur":"Turkish"}},{"cca2":"CZ","region":"Europe","unMember":true,"name":{"common":"Czechia"},"languages":{"ces":"Czech","slk":"Slovak"}},{"cca2":"DE","region":"Europe","unMember":true,"name":{"common":"Germany"},"languages":{"deu":"German"}},{"cca2":"DJ","region":"Africa","unMember":true,"name":{"common":"Djibouti"},"languages":{"ara":"Arabic","fra":"French"}},{"cca2":"DM","region":"Americas","unMember":true,"name":{"common":"Dominica"},"languages":{"eng":"English"}},{"cca2":"DK","region":"Europe","unMember":true,"name":{"common":"Denmark"},"languages":{"dan":"Danish"}},{"cca2":"DO","region":"Americas","unMember":true,"name":{"common":"Dominican Republic"},"languages":{"spa":"Spanish"}},{"cca2":"DZ","region":"Africa","unMember":true,"name":{"common":"Algeria"},"languages":{"ara":"Arabic"}},{"cca2":"EC","region":"Americas","unMember":true,"name":{"common":"Ecuador"},"languages":{"spa":"Spanish"}},{"cca2":"EG","region":"Africa","unMember":true,"name":{"common":"Egypt"},"languages":{"ara":"Arabic"}},{"cca2":"ER","region":"Africa","unMember":true,"name":{"common":"Eritrea"},"languages":{"ara":"Arabic","eng":"English","tir":"Tigrinya"}},{"cca2":"EH","region":"Africa","unMember":false,"name":{"common":"Western Sahara"},"languages":{"ber":"Berber","mey":"Hassaniya","spa":"Spanish"}},{"cca2":"ES","region":"Europe","unMember":true,"name":{"common":"Spain"},"languages":{"spa":"Spanish"}},{"cca2":"EE","region":"Europe","unMember":true,"name":{"common":"Estonia"},"languages":{"est":"Estonian"}},{"cca2":"ET","region":"Africa","unMember":true,"name":{"common":"Ethiopia"},"languages":{"amh":"Amharic"}},{"cca2":"FI","region":"Europe","unMember":true,"name":{"common":"Finland"},"languages":{"fin":"Finnish","swe":"Swedish"}},{"cca2":"FJ","region":"Oceania","unMember":true,"name":{"common":"Fiji"},"languages":{"eng":"English","fij":"Fijian","hif":"Fiji Hindi"}},{"cca2":"FK","region":"Americas","unMember":false,"name":{"common":"Falkland Islands"},"languages":{"eng":"English"}},{"cca2":"FR","region":"Europe","unMember":true,"name":{"common":"France"},"languages":{"fra":"French"}},{"cca2":"FO","region":"Europe","unMember":false,"name":{"common":"Faroe Islands"},"languages":{"dan":"Danish","fao":"Faroese"}},{"cca2":"FM","region":"Oceania","unMember":true,"name":{"common":"Micronesia"},"languages":{"eng":"English"}},{"cca2":"GA","region":"Africa","unMember":true,"name":{"common":"Gabon"},"languages":{"fra":"French"}},{"cca2":"GB","region":"Europe","unMember":true,"name":{"common":"United Kingdom"},"languages":{"eng":"English"}},{"cca2":"GE","region":"Asia","unMember":true,"name":{"common":"Georgia"},"languages":{"kat":"Georgian"}},{"cca2":"GG","region":"Europe","unMember":false,"name":{"common":"Guernsey"},"languages":{"eng":"English","fra":"French","nfr":"Guernésiais"}},{"cca2":"GH","region":"Africa","unMember":true,"name":{"common":"Ghana"},"languages":{"eng":"English"}},{"cca2":"GI","region":"Europe","unMember":false,"name":{"common":"Gibraltar"},"languages":{"eng":"English"}},{"cca2":"GN","region":"Africa","unMember":true,"name":{"common":"Guinea"},"languages":{"fra":"French"}},{"cca2":"GP","region":"Americas","unMember":false,"name":{"common":"Guadeloupe"},"languages":{"fra":"French"}},{"cca2":"GM","region":"Africa","unMember":true,"name":{"common":"Gambia"},"languages":{"eng":"English"}},{"cca2":"GW","region":"Africa","unMember":true,"name":{"common":"Guinea-Bissau"},"languages":{"por":"Portuguese","pov":"Upper Guinea Creole"}},{"cca2":"GQ","region":"Africa","unMember":true,"name":{"common":"Equatorial Guinea"},"languages":{"fra":"French","por":"Portuguese","spa":"Spanish"}},{"cca2":"GR","region":"Europe","unMember":true,"name":{"common":"Greece"},"languages":{"ell":"Greek"}},{"cca2":"GD","region":"Americas","unMember":true,"name":{"common":"Grenada"},"languages":{"eng":"English"}},{"cca2":"GL","region":"Americas","unMember":false,"name":{"common":"Greenland"},"languages":{"kal":"Greenlandic"}},{"cca2":"GT","region":"Americas","unMember":true,"name":{"common":"Guatemala"},"languages":{"spa":"Spanish"}},{"cca2":"GF","region":"Americas","unMember":false,"name":{"common":"French Guiana"},"languages":{"fra":"French"}},{"cca2":"GU","region":"Oceania","unMember":false,"name":{"common":"Guam"},"languages":{"cha":"Chamorro","eng":"English","spa":"Spanish"}},{"cca2":"GY","region":"Americas","unMember":true,"name":{"common":"Guyana"},"languages":{"eng":"English"}},{"cca2":"HK","region":"Asia","unMember":false,"name":{"common":"Hong Kong"},"languages":{"eng":"English","zho":"Chinese"}},{"cca2":"HM","region":"Antarctic","unMember":false,"name":{"common":"Heard Island and McDonald Islands"},"languages":{"eng":"English"}},{"cca2":"HN","region":"Americas","unMember":true,"name":{"common":"Honduras"},"languages":{"spa":"Spanish"}},{"cca2":"HR","region":"Europe","unMember":true,"name":{"common":"Croatia"},"languages":{"hrv":"Croatian"}},{"cca2":"HT","region":"Americas","unMember":true,"name":{"common":"Haiti"},"languages":{"fra":"French","hat":"Haitian Creole"}},{"cca2":"HU","region":"Europe","unMember":true,"name":{"common":"Hungary"},"languages":{"hun":"Hungarian"}},{"cca2":"ID","region":"Asia","unMember":true,"name":{"common":"Indonesia"},"languages":{"ind":"Indonesian"}},{"cca2":"IM","region":"Europe","unMember":false,"name":{"common":"Isle of Man"},"languages":{"eng":"English","glv":"Manx"}},{"cca2":"IN","region":"Asia","unMember":true,"name":{"common":"India"},"languages":{"eng":"English","hin":"Hindi","tam":"Tamil"}},{"cca2":"IO","region":"Africa","unMember":false,"name":{"common":"British Indian Ocean Territory"},"languages":{"eng":"English"}},{"cca2":"IE","region":"Europe","unMember":true,"name":{"common":"Ireland"},"languages":{"eng":"English","gle":"Irish"}},{"cca2":"IR","region":"Asia","unMember":true,"name":{"common":"Iran"},"languages":{"fas":"Persian (Farsi)"}},{"cca2":"IQ","region":"Asia","unMember":true,"name":{"common":"Iraq"},"languages":{"ara":"Arabic","arc":"Aramaic","ckb":"Sorani"}},{"cca2":"IS","region":"Europe","unMember":true,"name":{"common":"Iceland"},"languages":{"isl":"Icelandic"}},{"cca2":"IL","region":"Asia","unMember":true,"name":{"common":"Israel"},"languages":{"ara":"Arabic","heb":"Hebrew"}},{"cca2":"IT","region":"Europe","unMember":true,"name":{"common":"Italy"},"languages":{"ita":"Italian"}},{"cca2":"JM","region":"Americas","unMember":true,"name":{"common":"Jamaica"},"languages":{"eng":"English","jam":"Jamaican Patois"}},{"cca2":"JE","region":"Europe","unMember":false,"name":{"common":"Jersey"},"languages":{"eng":"English","fra":"French","nrf":"Jèrriais"}},{"cca2":"JO","region":"Asia","unMember":true,"name":{"common":"Jordan"},"languages":{"ara":"Arabic"}},{"cca2":"JP","region":"Asia","unMember":true,"name":{"common":"Japan"},"languages":{"jpn":"Japanese"}},{"cca2":"KZ","region":"Asia","unMember":true,"name":{"common":"Kazakhstan"},"languages":{"kaz":"Kazakh","rus":"Russian"}},{"cca2":"KE","region":"Africa","unMember":true,"name":{"common":"Kenya"},"languages":{"eng":"English","swa":"Swahili"}},{"cca2":"KG","region":"Asia","unMember":true,"name":{"common":"Kyrgyzstan"},"languages":{"kir":"Kyrgyz","rus":"Russian"}},{"cca2":"KH","region":"Asia","unMember":true,"name":{"common":"Cambodia"},"languages":{"khm":"Khmer"}},{"cca2":"KI","region":"Oceania","unMember":true,"name":{"common":"Kiribati"},"languages":{"eng":"English","gil":"Gilbertese"}},{"cca2":"KN","region":"Americas","unMember":true,"name":{"common":"Saint Kitts and Nevis"},"languages":{"eng":"English"}},{"cca2":"KR","region":"Asia","unMember":true,"name":{"common":"South Korea"},"languages":{"kor":"Korean"}},{"cca2":"XK","region":"Europe","unMember":false,"name":{"common":"Kosovo"},"languages":{"sqi":"Albanian","srp":"Serbian"}},{"cca2":"KW","region":"Asia","unMember":true,"name":{"common":"Kuwait"},"languages":{"ara":"Arabic"}},{"cca2":"LA","region":"Asia","unMember":true,"name":{"common":"Laos"},"languages":{"lao":"Lao"}},{"cca2":"LB","region":"Asia","unMember":true,"name":{"common":"Lebanon"},"languages":{"ara":"Arabic","fra":"French"}},{"cca2":"LR","region":"Africa","unMember":true,"name":{"common":"Liberia"},"languages":{"eng":"English"}},{"cca2":"LY","region":"Africa","unMember":true,"name":{"common":"Libya"},"languages":{"ara":"Arabic"}},{"cca2":"LC","region":"Americas","unMember":true,"name":{"common":"Saint Lucia"},"languages":{"eng":"English"}},{"cca2":"LI","region":"Europe","unMember":true,"name":{"common":"Liechtenstein"},"languages":{"deu":"German"}},{"cca2":"LK","region":"Asia","unMember":true,"name":{"common":"Sri Lanka"},"languages":{"sin":"Sinhala","tam":"Tamil"}},{"cca2":"LS","region":"Africa","unMember":true,"name":{"common":"Lesotho"},"languages":{"eng":"English","sot":"Sotho"}},{"cca2":"LT","region":"Europe","unMember":true,"name":{"common":"Lithuania"},"languages":{"lit":"Lithuanian"}},{"cca2":"LU","region":"Europe","unMember":true,"name":{"common":"Luxembourg"},"languages":{"deu":"German","fra":"French","ltz":"Luxembourgish"}},{"cca2":"LV","region":"Europe","unMember":true,"name":{"common":"Latvia"},"languages":{"lav":"Latvian"}},{"cca2":"MO","region":"Asia","unMember":false,"name":{"common":"Macau"},"languages":{"por":"Portuguese","zho":"Chinese"}},{"cca2":"MF","region":"Americas","unMember":false,"name":{"common":"Saint Martin"},"languages":{"fra":"French"}},{"cca2":"MA","region":"Africa","unMember":true,"name":{"common":"Morocco"},"languages":{"ara":"Arabic","ber":"Berber"}},{"cca2":"MC","region":"Europe","unMember":true,"name":{"common":"Monaco"},"languages":{"fra":"French"}},{"cca2":"MD","region":"Europe","unMember":true,"name":{"common":"Moldova"},"languages":{"ron":"Moldavian"}},{"cca2":"MG","region":"Africa","unMember":true,"name":{"common":"Madagascar"},"languages":{"fra":"French","mlg":"Malagasy"}},{"cca2":"MV","region":"Asia","unMember":true,"name":{"common":"Maldives"},"languages":{"div":"Maldivian"}},{"cca2":"MX","region":"Americas","unMember":true,"name":{"common":"Mexico"},"languages":{"spa":"Spanish"}},{"cca2":"MH","region":"Oceania","unMember":true,"name":{"common":"Marshall Islands"},"languages":{"eng":"English","mah":"Marshallese"}},{"cca2":"MK","region":"Europe","unMember":true,"name":{"common":"North Macedonia"},"languages":{"mkd":"Macedonian"}},{"cca2":"ML","region":"Africa","unMember":true,"name":{"common":"Mali"},"languages":{"fra":"French"}},{"cca2":"MT","region":"Europe","unMember":true,"name":{"common":"Malta"},"languages":{"eng":"English","mlt":"Maltese"}},{"cca2":"MM","region":"Asia","unMember":true,"name":{"common":"Myanmar"},"languages":{"mya":"Burmese"}},{"cca2":"ME","region":"Europe","unMember":true,"name":{"common":"Montenegro"},"languages":{"cnr":"Montenegrin"}},{"cca2":"MN","region":"Asia","unMember":true,"name":{"common":"Mongolia"},"languages":{"mon":"Mongolian"}},{"cca2":"MP","region":"Oceania","unMember":false,"name":{"common":"Northern Mariana Islands"},"languages":{"cal":"Carolinian","cha":"Chamorro","eng":"English"}},{"cca2":"MZ","region":"Africa","unMember":true,"name":{"common":"Mozambique"},"languages":{"por":"Portuguese"}},{"cca2":"MR","region":"Africa","unMember":true,"name":{"common":"Mauritania"},"languages":{"ara":"Arabic"}},{"cca2":"MS","region":"Americas","unMember":false,"name":{"common":"Montserrat"},"languages":{"eng":"English"}},{"cca2":"MQ","region":"Americas","unMember":false,"name":{"common":"Martinique"},"languages":{"fra":"French"}},{"cca2":"MU","region":"Africa","unMember":true,"name":{"common":"Mauritius"},"languages":{"eng":"English","fra":"French","mfe":"Mauritian Creole"}},{"cca2":"MW","region":"Africa","unMember":true,"name":{"common":"Malawi"},"languages":{"eng":"English","nya":"Chewa"}},{"cca2":"MY","region":"Asia","unMember":true,"name":{"common":"Malaysia"},"languages":{"eng":"English","msa":"Malay"}},{"cca2":"YT","region":"Africa","unMember":false,"name":{"common":"Mayotte"},"languages":{"fra":"French"}},{"cca2":"NA","region":"Africa","unMember":true,"name":{"common":"Namibia"},"languages":{"afr":"Afrikaans","deu":"German","eng":"English","her":"Herero","hgm":"Khoekhoe","kwn":"Kwangali","loz":"Lozi","ndo":"Ndonga","tsn":"Tswana"}},{"cca2":"NC","region":"Oceania","unMember":false,"name":{"common":"New Caledonia"},"languages":{"fra":"French"}},{"cca2":"NE","region":"Africa","unMember":true,"name":{"common":"Niger"},"languages":{"fra":"French"}},{"cca2":"NF","region":"Oceania","unMember":false,"name":{"common":"Norfolk Island"},"languages":{"eng":"English","pih":"Norfuk"}},{"cca2":"NG","region":"Africa","unMember":true,"name":{"common":"Nigeria"},"languages":{"eng":"English"}},{"cca2":"NI","region":"Americas","unMember":true,"name":{"common":"Nicaragua"},"languages":{"spa":"Spanish"}},{"cca2":"NU","region":"Oceania","unMember":false,"name":{"common":"Niue"},"languages":{"eng":"English","niu":"Niuean"}},{"cca2":"NL","region":"Europe","unMember":true,"name":{"common":"Netherlands"},"languages":{"nld":"Dutch"}},{"cca2":"NO","region":"Europe","unMember":true,"name":{"common":"Norway"},"languages":{"nno":"Norwegian Nynorsk","nob":"Norwegian Bokmål","smi":"Sami"}},{"cca2":"NP","region":"Asia","unMember":true,"name":{"common":"Nepal"},"languages":{"nep":"Nepali"}},{"cca2":"NR","region":"Oceania","unMember":true,"name":{"common":"Nauru"},"languages":{"eng":"English","nau":"Nauru"}},{"cca2":"NZ","region":"Oceania","unMember":true,"name":{"common":"New Zealand"},"languages":{"eng":"English","mri":"Māori","nzs":"New Zealand Sign Language"}},{"cca2":"OM","region":"Asia","unMember":true,"name":{"common":"Oman"},"languages":{"ara":"Arabic"}},{"cca2":"PK","region":"Asia","unMember":true,"name":{"common":"Pakistan"},"languages":{"eng":"English","urd":"Urdu"}},{"cca2":"PA","region":"Americas","unMember":true,"name":{"common":"Panama"},"languages":{"spa":"Spanish"}},{"cca2":"PN","region":"Oceania","unMember":false,"name":{"common":"Pitcairn Islands"},"languages":{"eng":"English"}},{"cca2":"PE","region":"Americas","unMember":true,"name":{"common":"Peru"},"languages":{"aym":"Aymara","que":"Quechua","spa":"Spanish"}},{"cca2":"PH","region":"Asia","unMember":true,"name":{"common":"Philippines"},"languages":{"eng":"English","fil":"Filipino"}},{"cca2":"PW","region":"Oceania","unMember":true,"name":{"common":"Palau"},"languages":{"eng":"English","pau":"Palauan"}},{"cca2":"PG","region":"Oceania","unMember":true,"name":{"common":"Papua New Guinea"},"languages":{"eng":"English","hmo":"Hiri Motu","tpi":"Tok Pisin"}},{"cca2":"PL","region":"Europe","unMember":true,"name":{"common":"Poland"},"languages":{"pol":"Polish"}},{"cca2":"PR","region":"Americas","unMember":false,"name":{"common":"Puerto Rico"},"languages":{"eng":"English","spa":"Spanish"}},{"cca2":"KP","region":"Asia","unMember":true,"name":{"common":"North Korea"},"languages":{"kor":"Korean"}},{"cca2":"PT","region":"Europe","unMember":true,"name":{"common":"Portugal"},"languages":{"por":"Portuguese"}},{"cca2":"PY","region":"Americas","unMember":true,"name":{"common":"Paraguay"},"languages":{"grn":"Guaraní","spa":"Spanish"}},{"cca2":"PS","region":"Asia","unMember":false,"name":{"common":"Palestine"},"languages":{"ara":"Arabic"}},{"cca2":"PF","region":"Oceania","unMember":false,"name":{"common":"French Polynesia"},"languages":{"fra":"French"}},{"cca2":"QA","region":"Asia","unMember":true,"name":{"common":"Qatar"},"languages":{"ara":"Arabic"}},{"cca2":"RE","region":"Africa","unMember":false,"name":{"common":"Réunion"},"languages":{"fra":"French"}},{"cca2":"RO","region":"Europe","unMember":true,"name":{"common":"Romania"},"languages":{"ron":"Romanian"}},{"cca2":"RU","region":"Europe","unMember":true,"name":{"common":"Russia"},"languages":{"rus":"Russian"}},{"cca2":"RW","region":"Africa","unMember":true,"name":{"common":"Rwanda"},"languages":{"eng":"English","fra":"French","kin":"Kinyarwanda"}},{"cca2":"SA","region":"Asia","unMember":true,"name":{"common":"Saudi Arabia"},"languages":{"ara":"Arabic"}},{"cca2":"SD","region":"Africa","unMember":true,"name":{"common":"Sudan"},"languages":{"ara":"Arabic","eng":"English"}},{"cca2":"SN","region":"Africa","unMember":true,"name":{"common":"Senegal"},"languages":{"fra":"French"}},{"cca2":"SG","region":"Asia","unMember":true,"name":{"common":"Singapore"},"languages":{"eng":"English","msa":"Malay","tam":"Tamil","zho":"Chinese"}},{"cca2":"GS","region":"Antarctic","unMember":false,"name":{"common":"South Georgia"},"languages":{"eng":"English"}},{"cca2":"SJ","region":"Europe","unMember":false,"name":{"common":"Svalbard and Jan Mayen"},"languages":{"nor":"Norwegian"}},{"cca2":"SB","region":"Oceania","unMember":true,"name":{"common":"Solomon Islands"},"languages":{"eng":"English"}},{"cca2":"SL","region":"Africa","unMember":true,"name":{"common":"Sierra Leone"},"languages":{"eng":"English"}},{"cca2":"SV","region":"Americas","unMember":true,"name":{"common":"El Salvador"},"languages":{"spa":"Spanish"}},{"cca2":"SM","region":"Europe","unMember":true,"name":{"common":"San Marino"},"languages":{"ita":"Italian"}},{"cca2":"SO","region":"Africa","unMember":true,"name":{"common":"Somalia"},"languages":{"ara":"Arabic","som":"Somali"}},{"cca2":"PM","region":"Americas","unMember":false,"name":{"common":"Saint Pierre and Miquelon"},"languages":{"fra":"French"}},{"cca2":"RS","region":"Europe","unMember":true,"name":{"common":"Serbia"},"languages":{"srp":"Serbian"}},{"cca2":"SS","region":"Africa","unMember":true,"name":{"common":"South Sudan"},"languages":{"eng":"English"}},{"cca2":"ST","region":"Africa","unMember":true,"name":{"common":"São Tomé and Príncipe"},"languages":{"por":"Portuguese"}},{"cca2":"SR","region":"Americas","unMember":true,"name":{"common":"Suriname"},"languages":{"nld":"Dutch"}},{"cca2":"SK","region":"Europe","unMember":true,"name":{"common":"Slovakia"},"languages":{"slk":"Slovak"}},{"cca2":"SI","region":"Europe","unMember":true,"name":{"common":"Slovenia"},"languages":{"slv":"Slovene"}},{"cca2":"SE","region":"Europe","unMember":true,"name":{"common":"Sweden"},"languages":{"swe":"Swedish"}},{"cca2":"SZ","region":"Africa","unMember":true,"name":{"common":"Eswatini"},"languages":{"eng":"English","ssw":"Swazi"}},{"cca2":"SX","region":"Americas","unMember":false,"name":{"common":"Sint Maarten"},"languages":{"eng":"English","fra":"French","nld":"Dutch"}},{"cca2":"SC","region":"Africa","unMember":true,"name":{"common":"Seychelles"},"languages":{"crs":"Seychellois Creole","eng":"English","fra":"French"}},{"cca2":"SY","region":"Asia","unMember":true,"name":{"common":"Syria"},"languages":{"ara":"Arabic"}},{"cca2":"TC","region":"Americas","unMember":false,"name":{"common":"Turks and Caicos Islands"},"languages":{"eng":"English"}},{"cca2":"TD","region":"Africa","unMember":true,"name":{"common":"Chad"},"languages":{"ara":"Arabic","fra":"French"}},{"cca2":"TG","region":"Africa","unMember":true,"name":{"common":"Togo"},"languages":{"fra":"French"}},{"cca2":"TH","region":"Asia","unMember":true,"name":{"common":"Thailand"},"languages":{"tha":"Thai"}},{"cca2":"TJ","region":"Asia","unMember":true,"name":{"common":"Tajikistan"},"languages":{"rus":"Russian","tgk":"Tajik"}},{"cca2":"TK","region":"Oceania","unMember":false,"name":{"common":"Tokelau"},"languages":{"eng":"English","smo":"Samoan","tkl":"Tokelauan"}},{"cca2":"TM","region":"Asia","unMember":true,"name":{"common":"Turkmenistan"},"languages":{"rus":"Russian","tuk":"Turkmen"}},{"cca2":"TL","region":"Asia","unMember":true,"name":{"common":"Timor-Leste"},"languages":{"por":"Portuguese","tet":"Tetum"}},{"cca2":"TO","region":"Oceania","unMember":true,"name":{"common":"Tonga"},"languages":{"eng":"English","ton":"Tongan"}},{"cca2":"TT","region":"Americas","unMember":true,"name":{"common":"Trinidad and Tobago"},"languages":{"eng":"English"}},{"cca2":"TN","region":"Africa","unMember":true,"name":{"common":"Tunisia"},"languages":{"ara":"Arabic"}},{"cca2":"TR","region":"Asia","unMember":true,"name":{"common":"Türkiye"},"languages":{"tur":"Turkish"}},{"cca2":"TV","region":"Oceania","unMember":true,"name":{"common":"Tuvalu"},"languages":{"eng":"English","tvl":"Tuvaluan"}},{"cca2":"TW","region":"Asia","unMember":false,"name":{"common":"Taiwan"},"languages":{"zho":"Chinese"}},{"cca2":"TZ","region":"Africa","unMember":true,"name":{"common":"Tanzania"},"languages":{"eng":"English","swa":"Swahili"}},{"cca2":"UG","region":"Africa","unMember":true,"name":{"common":"Uganda"},"languages":{"eng":"English","swa":"Swahili"}},{"cca2":"UA","region":"Europe","unMember":true,"name":{"common":"Ukraine"},"languages":{"ukr":"Ukrainian"}},{"cca2":"UM","region":"Americas","unMember":false,"name":{"common":"United States Minor Outlying Islands"},"languages":{"eng":"English"}},{"cca2":"UY","region":"Americas","unMember":true,"name":{"common":"Uruguay"},"languages":{"spa":"Spanish"}},{"cca2":"US","region":"Americas","unMember":true,"name":{"common":"United States"},"languages":{"eng":"English"}},{"cca2":"UZ","region":"Asia","unMember":true,"name":{"common":"Uzbekistan"},"languages":{"rus":"Russian","uzb":"Uzbek"}},{"cca2":"VA","region":"Europe","unMember":true,"name":{"common":"Vatican City"},"languages":{"ita":"Italian","lat":"Latin"}},{"cca2":"VC","region":"Americas","unMember":true,"name":{"common":"Saint Vincent and the Grenadines"},"languages":{"eng":"English"}},{"cca2":"VE","region":"Americas","unMember":true,"name":{"common":"Venezuela"},"languages":{"spa":"Spanish"}},{"cca2":"VG","region":"Americas","unMember":false,"name":{"common":"British Virgin Islands"},"languages":{"eng":"English"}},{"cca2":"VI","region":"Americas","unMember":false,"name":{"common":"United States Virgin Islands"},"languages":{"eng":"English"}},{"cca2":"VN","region":"Asia","unMember":true,"name":{"common":"Vietnam"},"languages":{"vie":"Vietnamese"}},{"cca2":"VU","region":"Oceania","unMember":true,"name":{"common":"Vanuatu"},"languages":{"bis":"Bislama","eng":"English","fra":"French"}},{"cca2":"WF","region":"Oceania","unMember":false,"name":{"common":"Wallis and Futuna"},"languages":{"fra":"French"}},{"cca2":"WS","region":"Oceania","unMember":true,"name":{"common":"Samoa"},"languages":{"eng":"English","smo":"Samoan"}},{"cca2":"YE","region":"Asia","unMember":true,"name":{"common":"Yemen"},"languages":{"ara":"Arabic"}},{"cca2":"ZA","region":"Africa","unMember":true,"name":{"common":"South Africa"},"languages":{"afr":"Afrikaans","eng":"English","nbl":"Southern Ndebele","nso":"Northern Sotho","sot":"Southern Sotho","ssw":"Swazi","tsn":"Tswana","tso":"Tsonga","ven":"Venda","xho":"Xhosa","zul":"Zulu"}},{"cca2":"ZM","region":"Africa","unMember":true,"name":{"common":"Zambia"},"languages":{"eng":"English"}},{"cca2":"ZW","region":"Africa","unMember":true,"name":{"common":"Zimbabwe"},"languages":{"bwg":"Chibarwe","eng":"English","kck":"Kalanga","khi":"Khoisan","ndc":"Ndau","nde":"Northern Ndebele","nya":"Chewa","sna":"Shona","sot":"Sotho","toi":"Tonga","tsn":"Tswana","tso":"Tsonga","ven":"Venda","xho":"Xhosa","zib":"Zimbabwean Sign Language"}}] diff --git a/data/reference/heritage-ids.json b/data/reference/heritage-ids.json new file mode 100644 index 0000000..157c210 --- /dev/null +++ b/data/reference/heritage-ids.json @@ -0,0 +1,1355 @@ +{ + "1-EC": "whs-galapagos-islands", + "2-EC": "whs-city-of-quito", + "3-DE": "whs-aachen-cathedral", + "4-CA": "whs-l-anse-aux-meadows-national-historic-site", + "8-TN": "whs-ichkeul-national-park", + "9-ET": "whs-simien-national-park", + "10-ET": "whs-lower-valley-of-the-awash", + "12-ET": "whs-tiya", + "13-ET": "whs-melka-kunture-and-balchit-archaeological-and-palaeontological-sites-in-the-highland-area-of-ethiopia", + "15-ET": "whs-aksum", + "17-ET": "whs-lower-valley-of-the-omo", + "18-ET": "whs-rock-hewn-churches-lalibela", + "19-ET": "whs-fasil-ghebbi-gondar-region", + "20-SY": "whs-ancient-city-of-damascus", + "21-SY": "whs-ancient-city-of-aleppo", + "22-SY": "whs-ancient-city-of-bosra", + "23-SY": "whs-site-of-palmyra", + "24-CA": "whs-nahanni-national-park", + "25-SN": "whs-djoudj-national-bird-sanctuary", + "26-SN": "whs-island-of-goree", + "27-US": "whs-mesa-verde-national-park", + "28-US": "whs-yellowstone-national-park", + "29-PL": "whs-historic-centre-of-krakow", + "30-PL": "whs-historic-centre-of-warsaw", + "31-PL": "whs-auschwitz-birkenau-german-nazi-concentration-and-extermination-camp-1940-1945", + "32-PL": "whs-wieliczka-and-bochnia-royal-salt-mines", + "33-BY": "whs-bialowieza-forest-by", + "33-PL": "whs-bialowieza-forest-pl", + "34-GH": "whs-forts-and-castles-volta-greater-accra-central-and-western-regions", + "35-GH": "whs-asante-traditional-buildings", + "36-TN": "whs-medina-of-tunis", + "37-TN": "whs-archaeological-site-of-carthage", + "38-TN": "whs-amphitheatre-of-el-jem", + "39-TZ": "whs-ngorongoro-conservation-area", + "42-BG": "whs-boyana-church", + "43-BG": "whs-madara-rider", + "44-BG": "whs-thracian-tomb-of-kazanlak", + "45-BG": "whs-rock-hewn-churches-of-ivanovo", + "55-NO": "whs-roros-mining-town-and-the-circumference", + "58-NO": "whs-urnes-stave-church", + "59-NO": "whs-bryggen", + "63-CD": "whs-virunga-national-park", + "64-GT": "whs-tikal-national-park", + "65-GT": "whs-antigua-guatemala", + "71-CA": "whs-dinosaur-provincial-park", + "72-CA": "whs-kluane-wrangell-st-elias-glacier-bay-tatshenshini-alsek-ca", + "72-US": "whs-kluane-wrangell-st-elias-glacier-bay-tatshenshini-alsek-us", + "75-US": "whs-grand-canyon-national-park", + "76-US": "whs-everglades-national-park", + "78-US": "whs-independence-hall", + "79-CY": "whs-paphos", + "80-FR": "whs-mont-saint-michel-and-its-bay", + "81-FR": "whs-chartres-cathedral", + "83-FR": "whs-palace-and-park-of-versailles", + "84-FR": "whs-vezelay-church-and-hill", + "85-FR": "whs-prehistoric-sites-and-decorated-caves-of-the-vezere-valley", + "86-EG": "whs-memphis-and-its-necropolis-the-pyramid-fields-from-giza-to-dahshur", + "87-EG": "whs-ancient-thebes-with-its-necropolis", + "88-EG": "whs-nubian-monuments-from-abu-simbel-to-philae", + "89-EG": "whs-historic-cairo", + "90-EG": "whs-abu-mena", + "91-IT": "whs-historic-centre-of-rome-the-properties-of-the-holy-see-in-that-city-enjoying-extraterritorial-rights-and-san-paolo-fuori-le-mura-it", + "91-VA": "whs-historic-centre-of-rome-the-properties-of-the-holy-see-in-that-city-enjoying-extraterritorial-rights-and-san-paolo-fuori-le-mura-va", + "93-IT": "whs-church-and-dominican-convent-of-santa-maria-delle-grazie-with-the-last-supper-by-leonardo-da-vinci", + "94-IT": "whs-rock-drawings-in-valcamonica", + "95-HR": "whs-old-city-of-dubrovnik", + "96-RS": "whs-stari-ras-and-sopocani", + "97-HR": "whs-historical-complex-of-split-with-the-palace-of-diocletian", + "98-HR": "whs-plitvice-lakes-national-park", + "99-AL": "whs-natural-and-cultural-heritage-of-the-ohrid-region-al", + "99-MK": "whs-natural-and-cultural-heritage-of-the-ohrid-region-mk", + "100-ME": "whs-durmitor-national-park", + "102-DZ": "whs-al-qal-a-of-beni-hammad", + "111-ET": "whs-bale-mountains-national-park", + "113-IR": "whs-tchogha-zanbil", + "114-IR": "whs-persepolis", + "115-IR": "whs-meidan-emam-esfahan", + "116-ML": "whs-old-towns-of-djenne", + "119-ML": "whs-timbuktu", + "120-NP": "whs-sagarmatha-national-park", + "121-NP": "whs-kathmandu-valley", + "124-BR": "whs-historic-town-of-ouro-preto", + "125-ME": "whs-natural-and-culturo-historical-region-of-kotor", + "129-HN": "whs-maya-site-of-copan", + "130-MT": "whs-al-saflieni-hypogeum", + "131-MT": "whs-city-of-valletta", + "132-MT": "whs-megalithic-temples-of-malta", + "134-US": "whs-redwood-national-and-state-parks", + "135-PA": "whs-fortifications-on-the-caribbean-side-of-panama-portobelo-san-lorenzo", + "136-CD": "whs-garamba-national-park", + "137-CD": "whs-kahuzi-biega-national-park", + "138-PK": "whs-archaeological-ruins-at-moenjodaro", + "139-PK": "whs-taxila", + "140-PK": "whs-buddhist-ruins-of-takht-i-bahi-and-neighbouring-city-remains-at-sahr-i-bahlol", + "143-PK": "whs-historical-monuments-at-makli-thatta", + "144-TZ": "whs-ruins-of-kilwa-kisiwani-and-ruins-of-songo-mnara", + "145-AR": "whs-los-glaciares-national-park", + "147-AU": "whs-kakadu-national-park", + "149-GT": "whs-archaeological-park-and-ruins-of-quirigua", + "150-US": "whs-mammoth-cave-national-park", + "151-US": "whs-olympic-national-park", + "153-SN": "whs-niokolo-koba-national-park", + "154-AU": "whs-great-barrier-reef", + "155-CI": "whs-mount-nimba-strict-nature-reserve-ci", + "155-GN": "whs-mount-nimba-strict-nature-reserve-gn", + "156-TZ": "whs-serengeti-national-park", + "157-CA": "whs-sgang-gwaay", + "158-CA": "whs-head-smashed-in-buffalo-jump", + "159-PA": "whs-darien-national-park", + "160-FR": "whs-palace-and-park-of-fontainebleau", + "162-FR": "whs-amiens-cathedral", + "163-FR": "whs-roman-theatre-and-its-surroundings-and-the-triumphal-arch-of-orange", + "164-FR": "whs-arles-roman-and-romanesque-monuments", + "165-FR": "whs-cistercian-abbey-of-fontenay", + "166-AU": "whs-sydney-opera-house", + "167-AU": "whs-willandra-lakes-region", + "168-DE": "whs-speyer-cathedral", + "169-DE": "whs-wurzburg-residence-with-the-court-gardens-and-residence-square", + "170-MA": "whs-medina-of-fez", + "171-PK": "whs-fort-and-shalamar-gardens-in-lahore", + "173-TZ": "whs-stone-town-of-zanzibar", + "174-IT": "whs-historic-centre-of-florence", + "175-IT": "whs-medici-villas-and-gardens-in-tuscany", + "179-DZ": "whs-tassili-n-ajjer", + "180-HT": "whs-national-history-park-citadel-sans-souci-ramiers", + "181-AU": "whs-tasmanian-wilderness", + "183-LY": "whs-archaeological-site-of-leptis-magna", + "184-LY": "whs-archaeological-site-of-sabratha", + "185-SC": "whs-aldabra-atoll", + "186-AU": "whs-lord-howe-island-group", + "187-DE": "whs-st-mary-s-cathedral-and-st-michael-s-church-at-hildesheim", + "188-DZ": "whs-m-zab-valley", + "189-BR": "whs-historic-centre-of-the-town-of-olinda", + "190-LY": "whs-archaeological-site-of-cyrene", + "191-DZ": "whs-djemila", + "192-YE": "whs-old-walled-city-of-shibam", + "193-DZ": "whs-tipasa", + "194-DZ": "whs-timgad", + "195-CI": "whs-tai-national-park", + "196-HN": "whs-rio-platano-biosphere-reserve", + "198-US": "whs-cahokia-mounds-state-historic-site", + "199-TZ": "whs-selous-game-reserve", + "200-LK": "whs-sacred-city-of-anuradhapura", + "201-LK": "whs-ancient-city-of-polonnaruwa", + "202-LK": "whs-ancient-city-of-sigiriya", + "203-FR": "whs-from-the-great-saltworks-of-salins-les-bains-to-the-royal-saltworks-of-arc-et-senans-the-production-of-open-pan-salt", + "204-CU": "whs-old-havana-and-its-fortification-system", + "205-CR": "whs-talamanca-range-la-amistad-reserves-la-amistad-national-park-cr", + "205-PA": "whs-talamanca-range-la-amistad-reserves-la-amistad-national-park-pa", + "206-PT": "whs-central-zone-of-the-town-of-angra-do-heroismo-in-the-azores", + "208-AF": "whs-cultural-landscape-and-archaeological-remains-of-the-bamiyan-valley", + "211-AF": "whs-minaret-and-archaeological-remains-of-jam", + "216-BG": "whs-rila-monastery", + "217-BG": "whs-ancient-city-of-nessebar", + "219-BG": "whs-srebarna-nature-reserve", + "225-BG": "whs-pirin-national-park", + "227-CI": "whs-comoe-national-park", + "228-FR": "whs-historic-centre-of-avignon-papal-palace-episcopal-ensemble-and-avignon-bridge", + "229-FR": "whs-place-stanislas-place-de-la-carriere-and-place-d-alliance-in-nancy", + "230-FR": "whs-abbey-church-of-saint-savin-sur-gartempe", + "231-IN": "whs-red-fort-complex", + "232-IN": "whs-humayun-s-tomb-delhi", + "233-IN": "whs-qutb-minar-and-its-monuments-delhi", + "234-IN": "whs-churches-and-convents-of-goa", + "239-IN": "whs-group-of-monuments-at-pattadakal", + "240-IN": "whs-khajuraho-group-of-monuments", + "241-IN": "whs-group-of-monuments-at-hampi", + "242-IN": "whs-ajanta-caves", + "243-IN": "whs-ellora-caves", + "244-IN": "whs-elephanta-caves", + "246-IN": "whs-sun-temple-konarak", + "247-IN": "whs-hill-forts-of-rajasthan", + "249-IN": "whs-group-of-monuments-at-mahabalipuram", + "250-IN": "whs-great-living-chola-temples", + "251-IN": "whs-agra-fort", + "252-IN": "whs-taj-mahal", + "255-IN": "whs-fatehpur-sikri", + "256-CA": "whs-wood-buffalo-national-park", + "258-FR": "whs-gulf-of-porto-calanche-of-piana-gulf-of-girolata-scandola-reserve", + "259-US": "whs-great-smoky-mountains-national-park", + "260-EC": "whs-sangay-national-park", + "261-SC": "whs-vallee-de-mai-nature-reserve", + "262-SD": "whs-sanganeb-marine-national-park-and-dungonab-bay-mukkawar-island-marine-national-park", + "263-PT": "whs-monastery-of-the-hieronymites-and-tower-of-belem-in-lisbon", + "264-PT": "whs-monastery-of-batalha", + "265-PT": "whs-convent-of-christ-in-tomar", + "266-US": "whs-la-fortaleza-and-san-juan-national-historic-site-in-puerto-rico", + "267-CH": "whs-old-city-of-berne", + "268-CH": "whs-abbey-of-st-gall", + "269-CH": "whs-benedictine-convent-of-st-john-at-mustair", + "271-DE": "whs-pilgrimage-church-of-wies", + "272-DE": "whs-hanseatic-city-of-lubeck", + "273-PE": "whs-city-of-cuzco", + "274-PE": "whs-historic-sanctuary-of-machu-picchu", + "275-AR": "whs-jesuit-missions-of-the-guaranis-san-ignacio-mini-santa-ana-nuestra-senora-de-loreto-and-santa-maria-mayor-argentina-ruins-of-sao-miguel-das-missoes-brazil-ar", + "275-BR": "whs-jesuit-missions-of-the-guaranis-san-ignacio-mini-santa-ana-nuestra-senora-de-loreto-and-santa-maria-mayor-argentina-ruins-of-sao-miguel-das-missoes-brazil-br", + "276-IQ": "whs-samarra-archaeological-city", + "277-IQ": "whs-hatra", + "278-IQ": "whs-babylon", + "280-CD": "whs-salonga-national-park", + "284-NP": "whs-chitwan-national-park", + "285-CO": "whs-port-fortresses-and-group-of-monuments-cartagena", + "286-VA": "whs-vatican-city", + "287-LY": "whs-rock-art-sites-of-tadrart-acacus", + "288-DE": "whs-castles-of-augustusburg-and-falkenlust-at-bruhl", + "289-MW": "whs-lake-malawi-national-park", + "292-DE": "whs-cologne-cathedral", + "293-LB": "whs-anjar", + "294-LB": "whs-baalbek", + "295-LB": "whs-byblos", + "299-LB": "whs-tyre", + "300-CA": "whs-historic-district-of-old-quebec", + "302-ZW": "whs-mana-pools-national-park-sapi-and-chewore-safari-areas", + "303-AR": "whs-iguazu-national-park", + "304-CA": "whs-canadian-rocky-mountain-parks", + "306-ZW": "whs-matobo-hills", + "307-US": "whs-statue-of-liberty", + "308-US": "whs-yosemite-national-park", + "309-BR": "whs-historic-centre-of-salvador-de-bahia", + "310-ES": "whs-cave-of-altamira-and-paleolithic-cave-art-of-northern-spain", + "311-ES": "whs-old-town-of-segovia-and-its-aqueduct", + "312-ES": "whs-monuments-of-oviedo-and-the-kingdom-of-the-asturias", + "313-ES": "whs-historic-centre-of-cordoba", + "314-ES": "whs-alhambra-generalife-and-albayzin-granada", + "316-ES": "whs-burgos-cathedral", + "318-ES": "whs-monastery-and-site-of-the-escurial-madrid", + "320-ES": "whs-works-of-antoni-gaudi", + "321-BD": "whs-historic-mosque-city-of-bagerhat", + "322-BD": "whs-ruins-of-the-buddhist-vihara-at-paharpur", + "323-BJ": "whs-royal-palaces-of-abomey", + "326-JO": "whs-petra", + "327-JO": "whs-quseir-amra", + "330-PE": "whs-chavin-archaeological-site", + "331-MA": "whs-medina-of-marrakesh", + "332-TN": "whs-punic-town-of-kerkuane-and-its-necropolis", + "333-PE": "whs-huascaran-national-park", + "334-BR": "whs-sanctuary-of-bom-jesus-do-congonhas", + "335-IN": "whs-nanda-devi-and-valley-of-flowers-national-parks", + "337-IN": "whs-kaziranga-national-park", + "338-IN": "whs-manas-wildlife-sanctuary", + "340-IN": "whs-keoladeo-national-park", + "344-FR": "whs-pont-du-gard-roman-aqueduct", + "345-FR": "whs-historic-fortified-city-of-carcassonne", + "347-ES": "whs-santiago-de-compostela-old-town", + "348-ES": "whs-old-town-of-avila-with-its-extra-muros-churches", + "351-CY": "whs-painted-churches-in-the-troodos-region", + "352-NO": "whs-rock-art-of-alta", + "353-US": "whs-chaco-culture", + "354-CA": "whs-waterton-glacier-international-peace-park-ca", + "354-US": "whs-waterton-glacier-international-peace-park-us", + "355-BR": "whs-iguacu-national-park", + "356-TR": "whs-historic-areas-of-istanbul", + "357-TR": "whs-goreme-national-park-and-the-rock-sites-of-cappadocia", + "358-TR": "whs-great-mosque-and-hospital-of-divrigi", + "359-BG": "whs-thracian-tomb-of-sveshtari", + "361-PT": "whs-historic-centre-of-evora", + "362-LY": "whs-old-town-of-ghadames", + "364-ZW": "whs-great-zimbabwe-national-monument", + "365-ZW": "whs-khami-ruins-national-monument", + "366-PE": "whs-chan-chan-archaeological-zone", + "367-DE": "whs-roman-monuments-cathedral-of-st-peter-and-church-of-our-lady-in-trier", + "368-AU": "whs-gondwana-rainforests-of-australia", + "369-GB": "whs-giant-s-causeway-and-causeway-coast", + "370-GB": "whs-durham-castle-and-cathedral", + "371-GB": "whs-ironbridge-gorge", + "372-GB": "whs-studley-royal-park-including-the-ruins-of-fountains-abbey", + "373-GB": "whs-stonehenge-avebury-and-associated-sites", + "374-GB": "whs-castles-and-town-walls-of-king-edward-in-gwynedd", + "377-TR": "whs-hattusha-the-hittite-capital", + "378-ES": "whs-mudejar-architecture-of-aragon", + "379-ES": "whs-historic-city-of-toledo", + "380-ES": "whs-garajonay-national-park", + "381-ES": "whs-old-city-of-salamanca", + "383-ES": "whs-cathedral-alcazar-and-archivo-de-indias-in-seville", + "384-ES": "whs-old-town-of-caceres", + "385-YE": "whs-old-city-of-sana-a", + "387-GB": "whs-st-kilda", + "389-RS": "whs-studenica-monastery", + "390-SI": "whs-skocjan-caves", + "392-GR": "whs-temple-of-apollo-epicurius-at-bassae", + "393-GR": "whs-archaeological-site-of-delphi", + "394-IT": "whs-venice-and-its-lagoon", + "395-IT": "whs-piazza-del-duomo-pisa", + "398-IT": "whs-castel-del-monte", + "400-HU": "whs-budapest-including-the-banks-of-the-danube-the-buda-castle-quarter-and-andrassy-avenue", + "401-HU": "whs-old-village-of-holloko-and-its-surroundings", + "402-PE": "whs-manu-national-park", + "403-TZ": "whs-kilimanjaro-national-park", + "404-GR": "whs-acropolis-athens", + "405-LK": "whs-sinharaja-forest-reserve", + "407-CM": "whs-dja-faunal-reserve", + "409-US": "whs-hawaii-volcanoes-national-park", + "410-MX": "whs-sian-ka-an", + "411-MX": "whs-pre-hispanic-city-and-national-park-of-palenque", + "412-MX": "whs-historic-centre-of-mexico-city-and-xochimilco", + "414-MX": "whs-pre-hispanic-city-of-teotihuacan", + "415-MX": "whs-historic-centre-of-oaxaca-and-archaeological-site-of-monte-alban", + "416-MX": "whs-historic-centre-of-puebla", + "417-ES": "whs-ibiza-biodiversity-and-culture", + "419-CA": "whs-gros-morne-national-park", + "420-BO": "whs-city-of-potosi", + "421-NZ": "whs-tongariro-national-park", + "422-GB": "whs-the-english-lake-district", + "425-GB": "whs-blenheim-palace", + "426-GB": "whs-palace-of-westminster-and-westminster-abbey-including-saint-margaret-s-church", + "428-GB": "whs-city-of-bath", + "429-GB": "whs-new-lanark", + "430-DE": "whs-frontiers-of-the-roman-empire-de", + "430-GB": "whs-frontiers-of-the-roman-empire-gb", + "433-OM": "whs-bahla-fort", + "434-OM": "whs-archaeological-sites-of-bat-al-khutm-and-al-ayn", + "437-CN": "whs-mount-taishan", + "438-CN": "whs-the-great-wall", + "439-CN": "whs-imperial-palaces-of-the-ming-and-qing-dynasties-in-beijing-and-shenyang", + "440-CN": "whs-mogao-caves", + "441-CN": "whs-mausoleum-of-the-first-qin-emperor", + "442-US": "whs-monticello-and-the-university-of-virginia-in-charlottesville", + "444-MA": "whs-ksar-of-ait-ben-haddou", + "445-BR": "whs-brasilia", + "447-AU": "whs-uluru-kata-tjuta-national-park", + "448-TR": "whs-nemrut-dag", + "449-CN": "whs-peking-man-site-at-zhoukoudian", + "450-LK": "whs-sacred-city-of-kandy", + "451-LK": "whs-old-town-of-galle-and-its-fortifications", + "452-IN": "whs-sundarbans-national-park", + "454-GR": "whs-mount-athos", + "455-GR": "whs-meteora", + "456-GR": "whs-paleochristian-and-byzantine-monuments-of-thessalonika", + "460-CU": "whs-trinidad-and-the-valley-de-los-ingenios", + "474-HU": "whs-hortobagy-national-park-the-puszta", + "475-CF": "whs-manovo-gounda-st-floris-national-park", + "476-MW": "whs-chongoni-rock-art-area", + "479-LA": "whs-town-of-luang-prabang", + "481-LA": "whs-vat-phou-and-associated-ancient-settlements-within-the-champasak-cultural-landscape", + "482-MX": "whs-historic-town-of-guanajuato-and-adjacent-mines", + "483-MX": "whs-pre-hispanic-city-of-chichen-itza", + "484-TR": "whs-xanthos-letoon", + "485-TR": "whs-hierapolis-pamukkale", + "486-AU": "whs-wet-tropics-of-queensland", + "487-GB": "whs-henderson-island", + "488-GB": "whs-tower-of-london", + "491-GR": "whs-sanctuary-of-asklepios-at-epidaurus", + "492-US": "whs-taos-pueblo", + "493-GR": "whs-medieval-city-of-rhodes", + "494-MG": "whs-andrefana-dry-forests", + "495-FR": "whs-strasbourg-grande-ile-and-neustadt", + "496-GB": "whs-canterbury-cathedral-st-augustine-s-abbey-and-st-martin-s-church", + "498-TN": "whs-medina-of-sousse", + "499-TN": "whs-kairouan", + "500-PE": "whs-historic-centre-of-lima", + "502-PH": "whs-historic-city-of-vigan", + "505-PT": "whs-monastery-of-alcobaca", + "506-MR": "whs-banc-d-arguin-national-park", + "509-ZM": "whs-mosi-oa-tunya-victoria-falls-zm", + "509-ZW": "whs-mosi-oa-tunya-victoria-falls-zw", + "511-GR": "whs-archaeological-site-of-mystras", + "514-GB": "whs-heart-of-neolithic-orkney", + "515-DE": "whs-abbey-and-altenmunster-of-lorsch", + "516-ML": "whs-cliff-of-bandiagara-land-of-the-dogons", + "517-GR": "whs-archaeological-site-of-olympia", + "518-ES": "whs-poblet-monastery", + "522-ES": "whs-renaissance-monumental-ensembles-of-ubeda-and-baeza", + "524-IN": "whs-buddhist-monuments-at-sanchi", + "526-DO": "whs-colonial-city-of-santo-domingo", + "527-UA": "whs-kyiv-saint-sophia-cathedral-and-related-monastic-buildings-kyiv-pechersk-lavra", + "529-BO": "whs-jesuit-missions-of-the-chiquitos", + "530-GR": "whs-delos", + "532-DE": "whs-palaces-and-parks-of-potsdam-and-berlin", + "534-DE": "whs-garden-kingdom-of-dessau-worlitz", + "535-DE": "whs-collegiate-church-castle-and-old-town-of-quedlinburg", + "537-GR": "whs-monasteries-of-daphni-hosios-loukas-and-nea-moni-of-chios", + "540-RU": "whs-historic-centre-of-saint-petersburg-and-related-groups-of-monuments", + "541-LT": "whs-vilnius-historic-centre", + "543-UZ": "whs-itchan-kala", + "544-RU": "whs-kizhi-pogost", + "545-RU": "whs-kremlin-and-red-square-moscow", + "546-DE": "whs-maulbronn-monastery-complex", + "547-CN": "whs-mount-huangshan", + "548-PE": "whs-rio-abiseo-national-park", + "549-IT": "whs-18th-century-royal-palace-at-caserta-with-the-park-the-aqueduct-of-vanvitelli-and-the-san-leucio-complex", + "550-IT": "whs-historic-centre-of-san-gimignano", + "551-NZ": "whs-te-wahipounamu-south-west-new-zealand", + "554-MX": "whs-whale-sanctuary-of-el-vizcaino", + "555-SE": "whs-birka-and-hovgarden", + "556-SE": "whs-engelsberg-ironworks", + "557-SE": "whs-rock-carvings-in-tanum", + "558-SE": "whs-skogskyrkogarden", + "559-SE": "whs-royal-domain-of-drottningholm", + "560-MX": "whs-archaeological-zone-of-paquime-casas-grandes", + "561-LK": "whs-rangiri-dambulla-cave-temple", + "564-PL": "whs-old-city-of-zamosc", + "565-DZ": "whs-kasbah-of-algiers", + "566-BO": "whs-historic-city-of-sucre", + "567-BO": "whs-tiwanaku-spiritual-and-political-centre-of-the-tiwanaku-culture", + "569-AL": "whs-historic-centres-of-berat-and-gjirokastra", + "570-AL": "whs-butrint", + "573-NE": "whs-air-and-tenere-natural-reserves", + "574-TH": "whs-historic-town-of-sukhothai-and-associated-historic-towns", + "575-TH": "whs-ban-chiang-archaeological-site", + "576-TH": "whs-historic-city-of-ayutthaya", + "577-AU": "whs-heard-and-mcdonald-islands", + "578-AU": "whs-shark-bay-western-australia", + "579-FI": "whs-bronze-age-burial-site-of-sammallahdenmaki", + "582-FI": "whs-old-rauma", + "583-FI": "whs-fortress-of-suomenlinna", + "584-FI": "whs-petajavesi-old-church", + "585-MX": "whs-historic-centre-of-morelia", + "586-PK": "whs-rohtas-fort", + "588-RO": "whs-danube-delta", + "590-TH": "whs-dong-phayayen-khao-yai-forest-complex", + "591-TH": "whs-thungyai-huai-kha-khaeng-wildlife-sanctuaries", + "592-ID": "whs-borobudur-temple-compounds", + "593-ID": "whs-sangiran-early-man-site", + "595-GR": "whs-pythagoreion-and-heraion-of-samos", + "596-RO": "whs-villages-with-fortified-churches-in-transylvania", + "597-RO": "whs-monastery-of-horezu", + "598-RO": "whs-churches-of-moldavia", + "599-MZ": "whs-island-of-mozambique", + "600-FR": "whs-paris-banks-of-the-seine", + "601-FR": "whs-cathedral-of-notre-dame-former-abbey-of-saint-remi-and-palace-of-tau-reims", + "602-UZ": "whs-historic-centre-of-bukhara", + "603-UZ": "whs-samarkand-crossroad-of-cultures", + "604-RU": "whs-historic-monuments-of-novgorod-and-surroundings", + "606-BR": "whs-serra-da-capivara-national-park", + "608-ID": "whs-ujung-kulon-national-park", + "609-ID": "whs-komodo-national-park", + "611-YE": "whs-historic-town-of-zabid", + "613-NI": "whs-ruins-of-leon-viejo", + "614-TR": "whs-city-of-safranbolu", + "616-CZ": "whs-historic-centre-of-prague", + "617-CZ": "whs-historic-centre-of-cesky-krumlov", + "618-SK": "whs-historic-town-of-banska-stiavnica-and-the-technical-monuments-in-its-vicinity", + "620-SK": "whs-levoca-spissky-hrad-and-the-associated-cultural-monuments", + "621-CZ": "whs-historic-centre-of-telc", + "622-SK": "whs-vlkolinec", + "623-DE": "whs-mines-of-rammelsberg-historic-town-of-goslar-and-upper-harz-water-management-system", + "624-DE": "whs-town-of-bamberg", + "625-BY": "whs-mir-castle-complex", + "629-AU": "whs-macquarie-island", + "630-AU": "whs-k-gari", + "631-MX": "whs-el-tajin-pre-hispanic-city", + "632-RU": "whs-cultural-and-historic-ensemble-of-the-solovetsky-islands", + "633-RU": "whs-white-monuments-of-vladimir-and-suzdal", + "634-RU": "whs-church-of-the-ascension-kolomenskoye", + "635-FR": "whs-bourges-cathedral", + "637-CN": "whs-jiuzhaigou-valley-scenic-and-historic-interest-area", + "638-CN": "whs-huanglong-scenic-and-historic-interest-area", + "640-CN": "whs-wulingyuan-scenic-and-historic-interest-area", + "642-ID": "whs-prambanan-temple-compounds", + "648-PY": "whs-jesuit-missions-of-la-santisima-trinidad-de-parana-and-jesus-de-tavarangue", + "652-PH": "whs-puerto-princesa-subterranean-river-national-park", + "653-PH": "whs-tubbataha-reefs-natural-park", + "657-RU": "whs-architectural-ensemble-of-the-trinity-sergius-lavra-in-sergiev-posad", + "658-VE": "whs-coro-and-its-port", + "659-IE": "whs-bru-na-boinne-archaeological-ensemble-of-the-bend-of-the-boyne", + "660-JP": "whs-buddhist-monuments-in-the-horyu-ji-area", + "661-JP": "whs-himeji-jo", + "662-JP": "whs-yakushima", + "663-JP": "whs-shirakami-sanchi", + "664-ES": "whs-archaeological-ensemble-of-merida", + "665-ES": "whs-royal-monastery-of-santa-maria-de-guadalupe", + "666-NP": "whs-lumbini-the-birthplace-of-the-lord-buddha", + "668-KH": "whs-angkor", + "669-ES": "whs-routes-of-santiago-de-compostela-camino-frances-and-routes-of-northern-spain", + "670-IT": "whs-the-sassi-and-the-park-of-the-rupestrian-churches-of-matera", + "672-VN": "whs-ha-long-bay-cat-ba-archipelago", + "675-SV": "whs-joya-de-ceren-archaeological-site", + "676-MX": "whs-historic-centre-of-zacatecas", + "677-PH": "whs-baroque-churches-of-the-philippines", + "678-VN": "whs-complex-of-hue-monuments", + "682-UG": "whs-bwindi-impenetrable-national-park", + "684-UG": "whs-rwenzori-mountains-national-park", + "685-ES": "whs-donana-national-park", + "686-CA": "whs-miguasha-national-park", + "687-DE": "whs-volklingen-ironworks", + "688-JP": "whs-historic-monuments-of-ancient-kyoto-kyoto-uji-and-otsu-cities", + "689-JO": "whs-as-salt-the-place-of-tolerance-and-urban-hospitality", + "690-CZ": "whs-pilgrimage-church-of-st-john-of-nepomuk-at-zelena-hora", + "692-CG": "whs-forest-massif-of-odzala-kokoua", + "695-DK": "whs-roskilde-cathedral", + "696-DK": "whs-kronborg-castle", + "697-DK": "whs-jelling-mounds-runic-stones-and-church", + "698-AU": "whs-australian-fossil-mammal-sites-riversleigh-naracoorte", + "699-LU": "whs-city-of-luxembourg-its-old-quarters-and-fortifications", + "700-PE": "whs-lines-and-geoglyphs-of-nasca-and-palpa", + "701-VE": "whs-canaima-national-park", + "702-MX": "whs-earliest-16th-century-monasteries-on-the-slopes-of-popocatepetl", + "703-CN": "whs-mountain-resort-and-its-outlying-temples-chengde", + "704-CN": "whs-temple-and-cemetery-of-confucius-and-the-kong-family-mansion-in-qufu", + "705-CN": "whs-ancient-building-complex-in-the-wudang-mountains", + "707-CN": "whs-historic-ensemble-of-the-potala-palace-lhasa", + "708-GE": "whs-historical-monuments-of-mtskheta", + "709-GE": "whs-upper-svaneti", + "710-GE": "whs-gelati-monastery", + "711-CO": "whs-los-katios-national-park", + "712-IT": "whs-city-of-vicenza-and-the-palladian-villas-of-the-veneto", + "714-MX": "whs-rock-paintings-of-the-sierra-de-san-francisco", + "715-CL": "whs-rapa-nui-national-park", + "717-IT": "whs-historic-centre-of-siena", + "718-CD": "whs-okapi-wildlife-reserve", + "719-RU": "whs-virgin-komi-forests", + "720-DE": "whs-messel-pit-fossil-site", + "721-US": "whs-carlsbad-caverns-national-park", + "722-PH": "whs-rice-terraces-of-the-philippine-cordilleras", + "723-PT": "whs-cultural-landscape-of-sintra", + "724-RS": "whs-medieval-monuments-in-kosovo", + "725-HU": "whs-caves-of-aggtelek-karst-and-slovak-karst-hu", + "725-SK": "whs-caves-of-aggtelek-karst-and-slovak-karst-sk", + "726-IT": "whs-historic-centre-of-naples", + "728-GB": "whs-old-and-new-towns-of-edinburgh", + "729-DE": "whs-bauhaus-and-its-sites-in-weimar-dessau-and-bernau", + "730-IT": "whs-crespi-d-adda", + "731-SE": "whs-hanseatic-town-of-visby", + "732-CZ": "whs-kutna-hora-historical-town-centre-with-the-church-of-st-barbara-and-the-cathedral-of-our-lady-at-sedlec", + "733-IT": "whs-ferrara-city-of-the-renaissance-and-its-po-delta", + "734-JP": "whs-historic-villages-of-shirakawa-go-and-gokayama", + "736-KR": "whs-seokguram-grotto-and-bulguksa-temple", + "737-KR": "whs-haeinsa-temple-janggyeong-panjeon-the-depositories-for-the-tripitaka-koreana-woodblocks", + "738-KR": "whs-jongmyo-shrine", + "739-NL": "whs-schokland-and-surroundings", + "740-GB": "whs-gough-and-inaccessible-islands", + "741-CA": "whs-old-town-lunenburg", + "742-CO": "whs-historic-centre-of-santa-cruz-de-mompox", + "743-CO": "whs-national-archeological-park-of-tierradentro", + "744-CO": "whs-san-agustin-archaeological-park", + "747-UY": "whs-historic-quarter-of-the-city-of-colonia-del-sacramento", + "749-BF": "whs-w-arly-pendjari-complex-bf", + "749-BJ": "whs-w-arly-pendjari-complex-bj", + "749-NE": "whs-w-arly-pendjari-complex-ne", + "750-MR": "whs-ancient-ksour-of-ouadane-chinguetti-tichitt-and-oualata", + "751-FI": "whs-verla-groundwood-and-board-mill", + "753-MA": "whs-medina-of-essaouira-formerly-mogador", + "754-RU": "whs-lake-baikal", + "755-PT": "whs-historic-centre-of-oporto-luiz-i-bridge-and-monastery-of-serra-do-pilar", + "757-IE": "whs-sceilg-mhichil", + "758-HU": "whs-millenary-benedictine-abbey-of-pannonhalma-and-its-natural-environment", + "759-NL": "whs-dutch-water-defence-lines", + "761-GM": "whs-kunta-kinteh-island-and-related-sites", + "762-SE": "whs-church-town-of-gammelstad-lulea", + "763-CZ": "whs-lednice-valtice-cultural-landscape", + "764-BZ": "whs-belize-barrier-reef-reserve-system", + "765-RU": "whs-volcanoes-of-kamchatka", + "766-RU": "whs-central-sikhote-alin", + "768-RU": "whs-golden-mountains-of-altai", + "769-MN": "whs-uvs-nuur-basin-mn", + "769-RU": "whs-uvs-nuur-basin-ru", + "770-FR": "whs-canal-du-midi", + "772-AT": "whs-ferto-neusiedlersee-cultural-landscape-at", + "772-HU": "whs-ferto-neusiedlersee-cultural-landscape-hu", + "773-ES": "whs-pyrenees-mont-perdu-es", + "773-FR": "whs-pyrenees-mont-perdu-fr", + "774-SE": "whs-laponian-area", + "775-JP": "whs-hiroshima-peace-memorial-genbaku-dome", + "776-JP": "whs-itsukushima-shinto-shrine", + "777-AM": "whs-monasteries-of-haghpat-and-sanahin", + "778-CN": "whs-lushan-national-park", + "779-CN": "whs-mount-emei-scenic-area-including-leshan-giant-buddha-scenic-area", + "780-GR": "whs-archaeological-site-of-aigai-modern-name-vergina", + "781-ES": "whs-historic-walled-town-of-cuenca", + "782-ES": "whs-la-lonja-de-la-seda-de-valencia", + "783-DE": "whs-luther-memorials-in-eisleben-and-wittenberg", + "784-AT": "whs-historic-centre-of-the-city-of-salzburg", + "785-AT": "whs-semmering-railway", + "786-AT": "whs-palace-and-gardens-of-schonbrunn", + "787-IT": "whs-the-trulli-of-alberobello", + "788-IT": "whs-early-christian-monuments-of-ravenna", + "789-IT": "whs-historic-centre-of-the-city-of-pienza", + "791-MX": "whs-pre-hispanic-town-of-uxmal", + "792-MX": "whs-historic-monuments-zone-of-queretaro", + "793-MA": "whs-historic-city-of-meknes", + "794-TN": "whs-dougga-thugga", + "795-GB": "whs-maritime-greenwich", + "797-IT": "whs-city-of-verona", + "798-BD": "whs-the-sundarbans", + "800-KE": "whs-mount-kenya-national-park-natural-forest", + "801-KE": "whs-lake-turkana-national-parks", + "803-ES": "whs-las-medulas", + "804-ES": "whs-palau-de-la-musica-catalana-and-hospital-de-sant-pau-barcelona", + "805-ES": "whs-san-millan-yuso-and-suso-monasteries", + "806-AT": "whs-hallstatt-dachstein-salzkammergut-cultural-landscape", + "809-HR": "whs-episcopal-complex-of-the-euphrasian-basilica-in-the-historic-centre-of-porec", + "810-HR": "whs-historic-city-of-trogir", + "811-CN": "whs-old-town-of-lijiang", + "812-CN": "whs-ancient-city-of-ping-yao", + "813-CN": "whs-classical-gardens-of-suzhou", + "814-DM": "whs-morne-trois-pitons-national-park", + "815-MX": "whs-hospicio-cabanas-guadalajara", + "816-KR": "whs-changdeokgung-palace-complex", + "817-KR": "whs-hwaseong-fortress", + "818-NL": "whs-mill-network-at-kinderdijk-elshout", + "819-NL": "whs-historic-area-of-willemstad-inner-city-and-harbour-curacao", + "820-CR": "whs-cocos-island-national-park", + "821-BR": "whs-historic-centre-of-sao-luis", + "822-EE": "whs-historic-centre-old-town-of-tallinn", + "823-IT": "whs-residences-of-the-royal-house-of-savoy", + "824-IT": "whs-botanical-garden-orto-botanico-padua", + "825-IT": "whs-archaeological-area-and-the-patriarchal-basilica-of-aquileia", + "826-IT": "whs-portovenere-cinque-terre-and-the-islands-palmaria-tino-and-tinetto", + "827-IT": "whs-cathedral-torre-civica-and-piazza-grande-modena", + "828-IT": "whs-historic-centre-of-urbino", + "829-IT": "whs-archaeological-areas-of-pompei-herculaneum-and-torre-annunziata", + "830-IT": "whs-costiera-amalfitana", + "831-IT": "whs-archaeological-area-of-agrigento", + "832-IT": "whs-villa-romana-del-casale", + "833-IT": "whs-su-nuraxi-di-barumini", + "835-PL": "whs-medieval-town-of-torun", + "836-MA": "whs-archaeological-site-of-volubilis", + "837-MA": "whs-medina-of-tetouan-formerly-known-as-titawin", + "839-CU": "whs-alejandro-de-humboldt-national-park", + "840-CU": "whs-vinales-valley", + "841-CU": "whs-san-pedro-de-la-roca-castle-santiago-de-cuba", + "842-IT": "whs-cilento-and-vallo-di-diano-national-park-with-the-archeological-sites-of-paestum-and-velia-and-the-certosa-di-padula", + "846-DE": "whs-classical-weimar", + "847-PL": "whs-castle-of-the-teutonic-order-in-malbork", + "848-CY": "whs-choirokoitia", + "849-TR": "whs-archaeological-site-of-troy", + "850-LB": "whs-ouadi-qadisha-the-holy-valley-and-the-forest-of-the-cedars-of-god-horsh-arz-el-rab", + "852-LV": "whs-historic-centre-of-riga", + "853-HU": "whs-early-christian-necropolis-of-pecs-sopianae", + "854-SB": "whs-east-rennell", + "855-BE": "whs-flemish-beguinages", + "856-BE": "whs-the-four-lifts-on-the-canal-du-centre-and-their-environs-la-louviere-and-le-roeulx-hainaut", + "857-BE": "whs-la-grand-place-brussels", + "859-CZ": "whs-holy-trinity-column-in-olomouc", + "860-CZ": "whs-gardens-and-castle-at-kromeriz", + "861-CZ": "whs-holasovice-historic-village", + "862-MX": "whs-historic-monuments-zone-of-tlacotalpan", + "863-EC": "whs-historic-centre-of-santa-ana-de-los-rios-de-cuenca", + "865-UA": "whs-l-viv-the-ensemble-of-the-historic-centre", + "866-ES": "whs-prehistoric-rock-art-sites-in-the-coa-valley-and-siega-verde-es", + "866-PT": "whs-prehistoric-rock-art-sites-in-the-coa-valley-and-siega-verde-pt", + "867-NL": "whs-ir-d-f-woudagemaal-d-f-wouda-steam-pumping-station", + "868-FR": "whs-routes-of-santiago-de-compostela-in-france", + "870-JP": "whs-historic-monuments-of-ancient-nara", + "871-SE": "whs-naval-port-of-karlskrona", + "872-FR": "whs-historic-site-of-lyon", + "873-FR": "whs-provins-town-of-medieval-fairs", + "874-ES": "whs-rock-art-of-the-mediterranean-basin-on-the-iberian-peninsula", + "875-ES": "whs-archaeological-ensemble-of-tarraco", + "876-ES": "whs-university-and-historic-precinct-of-alcala-de-henares", + "877-NZ": "whs-new-zealand-sub-antarctic-islands", + "880-CN": "whs-summer-palace-an-imperial-garden-in-beijing", + "881-CN": "whs-temple-of-heaven-an-imperial-sacrificial-altar-in-beijing", + "883-BO": "whs-fuerte-de-samaipata", + "884-CH": "whs-three-castles-defensive-wall-and-ramparts-of-the-market-town-of-bellinzona", + "885-UZ": "whs-historic-centre-of-shakhrisyabz", + "886-TM": "whs-state-historical-and-cultural-park-ancient-merv", + "887-PG": "whs-kuk-early-agricultural-site", + "889-CU": "whs-desembarco-del-granma-national-park", + "890-BR": "whs-historic-centre-of-the-town-of-diamantina", + "892-BR": "whs-discovery-coast-atlantic-forest-reserves", + "893-BR": "whs-atlantic-forest-south-east-reserves", + "895-MX": "whs-historic-fortified-town-of-campeche", + "896-DE": "whs-museumsinsel-museum-island-berlin", + "897-DE": "whs-wartburg-castle", + "898-FI": "whs-high-coast-kvarken-archipelago-fi", + "898-SE": "whs-high-coast-kvarken-archipelago-se", + "899-NL": "whs-droogmakerij-de-beemster-beemster-polder", + "900-RU": "whs-western-caucasus", + "901-CZ": "whs-litomysl-castle", + "902-RO": "whs-historic-centre-of-sighisoara", + "904-RO": "whs-wooden-churches-of-maramures", + "905-PL": "whs-kalwaria-zebrzydowska-the-mannerist-architectural-and-park-landscape-complex-and-pilgrimage-park", + "906-RO": "whs-dacian-fortresses-of-the-orastie-mountains", + "907-IT": "whs-villa-adriana-tivoli", + "908-IT": "whs-isole-eolie-aeolian-islands", + "910-KN": "whs-brimstone-hill-fortress-national-park", + "911-CN": "whs-mount-wuyi", + "912-CN": "whs-dazu-rock-carvings", + "913-JP": "whs-shrines-and-temples-of-nikko", + "914-MZ": "whs-isimangaliso-wetland-park-maputo-national-park-mz", + "914-ZA": "whs-isimangaliso-wetland-park-maputo-national-park-za", + "915-ZA": "whs-fossil-hominid-sites-of-south-africa", + "916-ZA": "whs-robben-island", + "917-AU": "whs-greater-blue-mountains-area", + "922-IN": "whs-rani-ki-vav-the-queen-s-stepwell-at-patan-gujarat", + "925-IN": "whs-rock-shelters-of-bhimbetka", + "928-CR": "whs-area-de-conservacion-guanacaste", + "929-ES": "whs-san-cristobal-de-la-laguna", + "930-ES": "whs-palmeral-of-elche", + "931-AT": "whs-city-of-graz-historic-centre-and-schloss-eggenberg", + "932-FR": "whs-jurisdiction-of-saint-emilion", + "933-FR": "whs-the-loire-valley-between-sully-sur-loire-and-chalonnes", + "934-PT": "whs-laurisilva-of-madeira", + "936-AR": "whs-cueva-de-las-manos-rio-pinturas", + "937-AR": "whs-peninsula-valdes", + "938-NG": "whs-sukur-cultural-landscape", + "939-MX": "whs-archaeological-monuments-zone-of-xochicalco", + "940-SR": "whs-historic-inner-city-of-paramaribo", + "941-GR": "whs-archaeological-sites-of-mycenae-and-tiryns", + "942-GR": "whs-the-historic-centre-chora-with-the-monastery-of-saint-john-the-theologian-and-the-cave-of-the-apocalypse-on-the-island-of-patmos", + "943-BE": "whs-belfries-of-belgium-and-france-be", + "943-FR": "whs-belfries-of-belgium-and-france-fr", + "944-IN": "whs-mountain-railways-of-india", + "945-IN": "whs-chhatrapati-shivaji-terminus-formerly-victoria-terminus", + "946-BA": "whs-old-bridge-area-of-the-old-city-of-mostar", + "948-VN": "whs-hoi-an-ancient-town", + "949-VN": "whs-my-son-sanctuary", + "950-MG": "whs-royal-hill-of-ambohimanga", + "951-LA": "whs-phong-nha-ke-bang-national-park-and-hin-nam-no-national-park-la", + "951-VN": "whs-phong-nha-ke-bang-national-park-and-hin-nam-no-national-park-vn", + "954-EG": "whs-saint-catherine-area", + "955-ID": "whs-lorentz-national-park", + "956-SN": "whs-island-of-saint-louis", + "958-AZ": "whs-walled-city-of-baku-with-the-shirvanshah-s-palace-and-maiden-tower", + "959-CL": "whs-historic-quarter-of-the-seaport-city-of-valparaiso", + "960-AM": "whs-monastery-of-geghard-and-the-upper-azat-valley", + "963-HR": "whs-the-cathedral-of-st-james-in-sibenik", + "965-NL": "whs-rietveld-schroderhuis-rietveld-schroder-house", + "966-AR": "whs-ischigualasto-talampaya-natural-parks", + "967-BO": "whs-noel-kempff-mercado-national-park", + "968-SE": "whs-agricultural-landscape-of-southern-oland", + "970-AT": "whs-wachau-cultural-landscape", + "971-CL": "whs-churches-of-chiloe", + "972-JP": "whs-gusuku-sites-and-related-properties-of-the-kingdom-of-ryukyu", + "973-SK": "whs-bardejov-town-conservation-reserve", + "974-DE": "whs-monastic-island-of-reichenau", + "975-DE": "whs-zollverein-coal-mine-industrial-complex-in-essen", + "976-KR": "whs-gyeongju-historic-areas", + "977-KR": "whs-gochang-hwasun-and-ganghwa-dolmen-sites", + "978-GR": "whs-old-town-of-corfu", + "980-RU": "whs-historic-and-architectural-complex-of-the-kazan-kremlin", + "981-RU": "whs-bolgar-historical-and-archaeological-complex", + "982-RU": "whs-ensemble-of-the-ferapontov-monastery", + "983-GB": "whs-historic-town-of-st-george-and-related-fortifications-bermuda", + "984-GB": "whs-blaenavon-industrial-landscape", + "985-LS": "whs-maloti-drakensberg-park-ls", + "985-ZA": "whs-maloti-drakensberg-park-za", + "986-VE": "whs-ciudad-universitaria-de-caracas", + "987-ES": "whs-roman-walls-of-lugo", + "988-ES": "whs-catalan-romanesque-churches-of-the-vall-de-boi", + "989-ES": "whs-archaeological-site-of-atapuerca", + "990-IT": "whs-assisi-the-basilica-of-san-francesco-and-other-franciscan-sites", + "993-BR": "whs-historic-centre-of-the-town-of-goias", + "994-LT": "whs-curonian-spit-lt", + "994-RU": "whs-curonian-spit-ru", + "995-AR": "whs-jesuit-block-and-estancias-of-cordoba", + "996-BE": "whs-historic-centre-of-brugge", + "998-BR": "whs-central-amazon-conservation-complex", + "999-BR": "whs-pantanal-conservation-area", + "1000-BR": "whs-brazilian-atlantic-islands-fernando-de-noronha-and-atol-das-rocas-reserves", + "1001-CN": "whs-mount-qingcheng-and-the-dujiangyan-irrigation-system", + "1002-CN": "whs-ancient-villages-in-southern-anhui-xidi-and-hongcun", + "1003-CN": "whs-longmen-grottoes", + "1004-CN": "whs-imperial-tombs-of-the-ming-and-qing-dynasties", + "1005-BE": "whs-major-town-houses-of-the-architect-victor-horta-brussels", + "1006-BE": "whs-neolithic-flint-mines-at-spiennes-mons", + "1007-ZA": "whs-cape-floral-region-protected-areas", + "1008-CU": "whs-archaeological-landscape-of-the-first-coffee-plantations-in-the-south-east-of-cuba", + "1009-BE": "whs-notre-dame-cathedral-in-tournai", + "1010-OM": "whs-land-of-frankincense", + "1011-AM": "whs-cathedral-and-churches-of-echmiatsin-and-the-archaeological-site-of-zvartnots", + "1012-MY": "whs-kinabalu-park", + "1013-MY": "whs-gunung-mulu-national-park", + "1014-MY": "whs-the-archaeological-heritage-of-niah-national-park-s-caves-complex", + "1016-PE": "whs-historical-centre-of-the-city-of-arequipa", + "1017-SR": "whs-central-suriname-nature-reserve", + "1018-TR": "whs-ephesus", + "1021-BW": "whs-tsodilo", + "1022-UG": "whs-tombs-of-buganda-kings-at-kasubi", + "1023-RU": "whs-natural-system-of-wrangel-island-reserve", + "1024-IT": "whs-late-baroque-towns-of-the-val-di-noto-south-eastern-sicily", + "1025-IT": "whs-villa-d-este-tivoli", + "1026-IT": "whs-val-d-orcia", + "1027-SE": "whs-mining-area-of-the-great-copper-mountain-in-falun", + "1028-GB": "whs-saltaire", + "1029-GB": "whs-dorset-and-east-devon-coast", + "1030-GB": "whs-derwent-valley-mills", + "1031-PT": "whs-historic-centre-of-guimaraes-and-couros-zone", + "1033-AT": "whs-historic-centre-of-vienna", + "1035-BR": "whs-cerrado-protected-areas-chapada-dos-veadeiros-and-emas-national-parks", + "1037-CH": "whs-swiss-alps-jungfrau-aletsch", + "1039-CN": "whs-yungang-grottoes", + "1040-IL": "whs-masada", + "1042-IL": "whs-old-city-of-acre", + "1044-ES": "whs-aranjuez-cultural-landscape", + "1046-PT": "whs-alto-douro-wine-region", + "1052-CZ": "whs-tugendhat-villa-in-brno", + "1053-PL": "whs-wooden-churches-of-southern-malopolska", + "1054-PL": "whs-churches-of-peace-in-jawor-and-swidnica", + "1055-KE": "whs-lamu-old-town", + "1056-IN": "whs-mahabodhi-temple-complex-at-bodh-gaya", + "1058-MA": "whs-portuguese-city-of-mazagan-el-jadida", + "1060-KE": "whs-kenya-lake-system-in-the-great-rift-valley", + "1061-MX": "whs-ancient-maya-city-and-protected-tropical-forests-of-calakmul-campeche", + "1063-HU": "whs-tokaj-wine-region-historic-cultural-landscape", + "1066-DE": "whs-upper-middle-rhine-valley", + "1067-DE": "whs-historic-centres-of-stralsund-and-wismar", + "1068-IT": "whs-sacri-monti-of-piedmont-and-lombardy", + "1070-RU": "whs-citadel-ancient-city-and-fortress-buildings-of-derbent", + "1073-SD": "whs-gebel-barkal-and-the-sites-of-the-napatan-region", + "1076-AZ": "whs-gobustan-rock-art-cultural-landscape", + "1077-IR": "whs-takht-e-soleyman", + "1078-CZ": "whs-jewish-quarter-and-st-procopius-basilica-in-trebic", + "1079-MX": "whs-franciscan-missions-in-the-sierra-gorda-of-queretaro", + "1081-MN": "whs-orkhon-valley-cultural-landscape", + "1083-CN": "whs-three-parallel-rivers-of-yunnan-protected-areas", + "1084-GB": "whs-royal-botanic-gardens-kew", + "1087-DE": "whs-town-hall-and-roland-on-the-marketplace-of-bremen", + "1090-CH": "whs-monte-san-giorgio-ch", + "1090-IT": "whs-monte-san-giorgio-it", + "1091-KP": "whs-complex-of-koguryo-tombs", + "1093-JO": "whs-um-er-rasas-kastrom-mefa-a", + "1094-AU": "whs-purnululu-national-park", + "1096-IL": "whs-white-city-of-tel-aviv-the-modern-movement", + "1097-RU": "whs-ensemble-of-the-novodevichy-convent", + "1099-ZA": "whs-mapungubwe-cultural-landscape", + "1100-BR": "whs-rio-de-janeiro-carioca-landscapes-between-the-mountain-and-the-sea", + "1101-IN": "whs-champaner-pavagadh-archaeological-park", + "1102-KZ": "whs-saryarka-steppe-and-lakes-of-northern-kazakhstan", + "1103-KZ": "whs-mausoleum-of-khoja-ahmed-yasawi", + "1106-IR": "whs-pasargadae", + "1107-IL": "whs-incense-route-desert-cities-in-the-negev", + "1108-IL": "whs-biblical-tels-megiddo-hazor-beer-sheba", + "1110-CN": "whs-historic-centre-of-macao", + "1111-CN": "whs-cultural-landscape-of-honghe-hani-rice-terraces", + "1112-CN": "whs-kaiping-diaolou-and-villages", + "1113-CN": "whs-fujian-tulou", + "1114-CN": "whs-yin-xu", + "1115-FR": "whs-lagoons-of-new-caledonia-reef-diversity-and-associated-ecosystems", + "1116-AR": "whs-quebrada-de-humahuaca", + "1117-PT": "whs-landscape-of-the-pico-island-vineyard-culture", + "1118-NG": "whs-osun-osogbo-sacred-grove", + "1121-CO": "whs-coffee-cultural-landscape-of-colombia", + "1127-DE": "whs-muskauer-park-park-muzakowski-de", + "1127-PL": "whs-muskauer-park-park-muzakowski-pl", + "1130-IQ": "whs-ashur-qal-at-sherqat", + "1131-AU": "whs-royal-exhibition-building-and-carlton-gardens", + "1133-AL": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-al", + "1133-AT": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-at", + "1133-BA": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-ba", + "1133-BE": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-be", + "1133-BG": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-bg", + "1133-CH": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-ch", + "1133-CZ": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-cz", + "1133-DE": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-de", + "1133-ES": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-es", + "1133-FR": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-fr", + "1133-HR": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-hr", + "1133-IT": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-it", + "1133-MK": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-mk", + "1133-PL": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-pl", + "1133-RO": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-ro", + "1133-SI": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-si", + "1133-SK": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-sk", + "1133-UA": "whs-ancient-and-primeval-beech-forests-of-the-carpathians-and-other-regions-of-europe-ua", + "1134-SE": "whs-grimeton-radio-station-varberg", + "1135-CN": "whs-capital-cities-and-tombs-of-the-ancient-koguryo-kingdom", + "1136-MX": "whs-luis-barragan-house-and-studio", + "1137-LT": "whs-kernave-archaeological-site-cultural-reserve-of-kernave", + "1138-PA": "whs-coiba-national-park-and-its-special-zone-of-marine-protection", + "1139-ML": "whs-tomb-of-askia", + "1140-BJ": "whs-koutammakou-the-land-of-the-batammariba-bj", + "1140-TG": "whs-koutammakou-the-land-of-the-batammariba-tg", + "1141-TJ": "whs-proto-urban-site-of-sarazm", + "1142-JP": "whs-sacred-sites-and-pilgrimage-routes-in-the-kii-mountain-range", + "1143-NO": "whs-vegaoyan-the-vega-archipelago", + "1145-KZ": "whs-petroglyphs-of-the-archaeological-landscape-of-tanbaly", + "1147-GA": "whs-ecosystem-and-relict-cultural-landscape-of-lope-okanda", + "1149-DK": "whs-ilulissat-icefjord", + "1152-IS": "whs-thingvellir-national-park", + "1153-FR": "whs-the-causses-and-the-cevennes-mediterranean-agro-pastoral-cultural-landscape", + "1155-DE": "whs-old-town-of-regensburg-with-stadtamhof", + "1158-IT": "whs-etruscan-necropolises-of-cerveteri-and-tarquinia", + "1160-AD": "whs-madriu-perafita-claror-valley", + "1161-LC": "whs-pitons-management-area", + "1162-ZA": "whs-vredefort-dome", + "1165-PL": "whs-centennial-hall-in-wroclaw", + "1167-ID": "whs-tropical-rainforest-heritage-of-sumatra", + "1170-RU": "whs-historical-centre-of-the-city-of-yaroslavl", + "1174-CO": "whs-chiribiquete-national-park-the-maloca-of-the-jaguar", + "1178-CL": "whs-humberstone-and-santa-laura-saltpeter-works", + "1179-CH": "whs-swiss-tectonic-arena-sardona", + "1181-FR": "whs-le-havre-the-city-rebuilt-by-auguste-perret", + "1182-MX": "whs-islands-and-protected-areas-of-the-gulf-of-california", + "1183-TZ": "whs-kondoa-rock-art-sites", + "1185-BE": "whs-plantin-moretus-house-workshops-museum-complex", + "1186-EG": "whs-wadi-al-hitan-whale-valley", + "1187-BY": "whs-struve-geodetic-arc-by", + "1187-EE": "whs-struve-geodetic-arc-ee", + "1187-FI": "whs-struve-geodetic-arc-fi", + "1187-LT": "whs-struve-geodetic-arc-lt", + "1187-LV": "whs-struve-geodetic-arc-lv", + "1187-MD": "whs-struve-geodetic-arc-md", + "1187-NO": "whs-struve-geodetic-arc-no", + "1187-RU": "whs-struve-geodetic-arc-ru", + "1187-SE": "whs-struve-geodetic-arc-se", + "1187-UA": "whs-struve-geodetic-arc-ua", + "1188-IR": "whs-soltaniyeh", + "1189-ET": "whs-harar-jugol-the-fortified-historic-town", + "1192-BH": "whs-qal-at-al-bahrain-ancient-harbour-and-capital-of-dilmun", + "1193-JP": "whs-shiretoko", + "1194-ID": "whs-cultural-landscape-of-bali-province-the-subak-system-as-a-manifestation-of-the-tri-hita-karana-philosophy", + "1195-NO": "whs-west-norwegian-fjords-geirangerfjord-and-naeroyfjord", + "1196-BY": "whs-architectural-residential-and-cultural-complex-of-the-radziwill-family-at-nesvizh", + "1199-TM": "whs-kunya-urgench", + "1200-IT": "whs-syracuse-and-the-rocky-necropolis-of-pantalica", + "1201-MW": "whs-mount-mulanje-cultural-landscape", + "1202-CU": "whs-urban-historic-centre-of-cienfuegos", + "1203-LK": "whs-central-highlands-of-sri-lanka", + "1207-OM": "whs-aflaj-irrigation-systems-of-oman", + "1208-IR": "whs-bam-and-its-cultural-landscape", + "1209-MX": "whs-agave-landscape-and-ancient-industrial-facilities-of-tequila", + "1211-IT": "whs-genoa-le-strade-nuove-and-the-system-of-the-palazzi-dei-rolli", + "1213-CN": "whs-sichuan-giant-panda-sanctuaries-wolong-mt-siguniang-and-jiajin-mountains", + "1214-CL": "whs-sewell-mining-town", + "1215-GB": "whs-cornwall-and-west-devon-mining-landscape", + "1216-CO": "whs-malpelo-fauna-and-flora-sanctuary", + "1217-ES": "whs-vizcaya-bridge", + "1220-IL": "whs-baha-i-holy-places-in-haifa-and-the-western-galilee", + "1221-CA": "whs-rideau-canal", + "1222-IR": "whs-bisotun", + "1223-MY": "whs-melaka-and-george-town-historic-cities-of-the-straits-of-malacca", + "1224-KH": "whs-temple-of-preah-vihear", + "1225-BF": "whs-ruins-of-loropeni", + "1226-GM": "whs-stone-circles-of-senegambia-gm", + "1226-SN": "whs-stone-circles-of-senegambia-sn", + "1227-MU": "whs-aapravasi-ghat", + "1229-SY": "whs-crac-des-chevaliers-and-qal-at-salah-el-din", + "1230-KG": "whs-sulaiman-too-sacred-mountain", + "1231-KE": "whs-sacred-mijikenda-kaya-forests", + "1234-RU": "whs-putorana-plateau", + "1236-NI": "whs-leon-cathedral", + "1237-IT": "whs-the-dolomites", + "1239-DE": "whs-berlin-modernism-housing-estates", + "1240-HR": "whs-stari-grad-plain", + "1242-TM": "whs-parthian-fortresses-of-nisa", + "1243-CH": "whs-lavaux-vineyard-terraces", + "1245-SM": "whs-san-marino-historic-centre-and-mount-titano", + "1246-JP": "whs-iwami-ginzan-silver-mine-and-its-cultural-landscape", + "1248-CN": "whs-south-china-karst", + "1250-MX": "whs-central-university-city-campus-of-the-universidad-nacional-autonoma-de-mexico-unam", + "1252-TJ": "whs-tajik-national-park-mountains-of-the-pamirs", + "1253-RS": "whs-gamzigrad-romuliana-palace-of-galerius", + "1255-NA": "whs-twyfelfontein-or-ui-aes", + "1256-FR": "whs-bordeaux-port-of-the-moon", + "1257-MG": "whs-rainforests-of-the-atsinanana", + "1258-ES": "whs-teide-national-park", + "1259-MU": "whs-le-morne-cultural-landscape", + "1260-BA": "whs-mehmed-pasa-sokolovic-bridge-in-visegrad", + "1262-IR": "whs-armenian-monastic-ensembles-of-iran", + "1263-YE": "whs-socotra-archipelago", + "1264-KR": "whs-jeju-volcanic-island-and-lava-tubes", + "1265-ZA": "whs-richtersveld-cultural-and-botanical-landscape", + "1267-IS": "whs-surtsey", + "1268-NE": "whs-historic-centre-of-agadez", + "1269-PE": "whs-sacred-city-of-caral-supe", + "1270-CU": "whs-historic-centre-of-camaguey", + "1272-BR": "whs-sao-francisco-square-in-the-town-of-sao-cristovao", + "1273-SK": "whs-wooden-churches-of-the-slovak-part-of-the-carpathian-mountain-area", + "1274-MX": "whs-protective-town-of-san-miguel-and-the-sanctuary-of-jesus-nazareno-de-atotonilco", + "1276-CH": "whs-rhaetian-railway-in-the-albula-bernina-landscapes-ch", + "1276-IT": "whs-rhaetian-railway-in-the-albula-bernina-landscapes-it", + "1277-JP": "whs-hiraizumi-temples-gardens-and-archaeological-sites-representing-the-buddhist-pure-land", + "1278-KP": "whs-historic-monuments-and-sites-in-kaesong", + "1279-CN": "whs-mount-wutai", + "1280-VU": "whs-chief-roi-mata-s-domain", + "1282-SE": "whs-decorated-farmhouses-of-halsingland", + "1283-FR": "whs-fortifications-of-vauban", + "1285-CA": "whs-joggins-fossil-cliffs", + "1287-IT": "whs-mantua-and-sabbioneta", + "1290-MX": "whs-monarch-butterfly-biosphere-reserve", + "1292-CN": "whs-mount-sanqingshan-national-park", + "1293-SA": "whs-hegra-archaeological-site-al-hijr-mada-in-salih", + "1295-KE": "whs-fort-jesus-mombasa", + "1298-BE": "whs-stoclet-house", + "1299-RU": "whs-lena-pillars-nature-park", + "1302-CH": "whs-la-chaux-de-fonds-le-locle-watchmaking-town-planning", + "1303-GB": "whs-pontcysyllte-aqueduct-and-canal", + "1305-CN": "whs-historic-monuments-of-dengfeng-in-the-centre-of-heaven-and-earth", + "1306-AU": "whs-australian-convict-sites", + "1308-BR": "whs-paraty-and-ilha-grande-culture-and-biodiversity", + "1310-CV": "whs-cidade-velha-historic-centre-of-ribeira-grande", + "1312-ES": "whs-tower-of-hercules", + "1313-ES": "whs-heritage-of-mercury-almaden-and-idrija-es", + "1313-SI": "whs-heritage-of-mercury-almaden-and-idrija-si", + "1314-DE": "whs-wadden-sea-de", + "1314-DK": "whs-wadden-sea-dk", + "1314-NL": "whs-wadden-sea-nl", + "1315-IR": "whs-shushtar-historical-hydraulic-system", + "1317-FR": "whs-pitons-cirques-and-remparts-of-reunion-island", + "1318-IT": "whs-longobards-in-italy-places-of-the-power-568-774-a-d", + "1319-KR": "whs-royal-tombs-of-the-joseon-dynasty", + "1321-AR": "whs-the-architectural-work-of-le-corbusier-an-outstanding-contribution-to-the-modern-movement-ar", + "1321-BE": "whs-the-architectural-work-of-le-corbusier-an-outstanding-contribution-to-the-modern-movement-be", + "1321-CH": "whs-the-architectural-work-of-le-corbusier-an-outstanding-contribution-to-the-modern-movement-ch", + "1321-DE": "whs-the-architectural-work-of-le-corbusier-an-outstanding-contribution-to-the-modern-movement-de", + "1321-FR": "whs-the-architectural-work-of-le-corbusier-an-outstanding-contribution-to-the-modern-movement-fr", + "1321-IN": "whs-the-architectural-work-of-le-corbusier-an-outstanding-contribution-to-the-modern-movement-in", + "1321-JP": "whs-the-architectural-work-of-le-corbusier-an-outstanding-contribution-to-the-modern-movement-jp", + "1322-CI": "whs-historic-town-of-grand-bassam", + "1324-KR": "whs-historic-villages-of-korea-hahoe-and-yangdong", + "1325-KI": "whs-phoenix-islands-protected-area", + "1326-US": "whs-papahanaumokuakea", + "1328-VN": "whs-central-sector-of-the-imperial-citadel-of-thang-long-hanoi", + "1329-SA": "whs-at-turaif-district-in-ad-dir-iyah", + "1330-UA": "whs-residence-of-bukovinian-and-dalmatian-metropolitans", + "1333-ET": "whs-konso-cultural-landscape", + "1334-CN": "whs-west-lake-cultural-landscape-of-hangzhou", + "1335-CN": "whs-china-danxia", + "1336-SD": "whs-archaeological-sites-of-the-island-of-meroe", + "1337-FR": "whs-episcopal-city-of-albi", + "1338-IN": "whs-the-jantar-mantar-jaipur", + "1339-MH": "whs-bikini-atoll-nuclear-test-site", + "1342-IN": "whs-western-ghats", + "1343-AE": "whs-cultural-sites-of-al-ain-hafit-hili-bidaa-bint-saud-and-oases-areas", + "1344-BE": "whs-major-mining-sites-of-wallonia", + "1345-IR": "whs-sheikh-safi-al-din-khanegah-and-shrine-ensemble-in-ardabil", + "1346-IR": "whs-tabriz-historic-bazaar-complex", + "1348-SY": "whs-ancient-villages-of-northern-syria", + "1349-NL": "whs-seventeenth-century-canal-ring-area-of-amsterdam-inside-the-singelgracht", + "1351-MX": "whs-camino-real-de-tierra-adentro", + "1352-MX": "whs-prehistoric-caves-of-yagul-and-mitla-in-the-central-valley-of-oaxaca", + "1356-JM": "whs-blue-and-john-crow-mountains", + "1358-VN": "whs-citadel-of-the-ho-dynasty", + "1359-SN": "whs-saloum-delta", + "1360-FR": "whs-nord-pas-de-calais-mining-basin", + "1361-SA": "whs-historic-jeddah-the-gate-to-makkah", + "1362-JP": "whs-ogasawara-islands", + "1363-AT": "whs-prehistoric-pile-dwellings-around-the-alps-at", + "1363-CH": "whs-prehistoric-pile-dwellings-around-the-alps-ch", + "1363-DE": "whs-prehistoric-pile-dwellings-around-the-alps-de", + "1363-FR": "whs-prehistoric-pile-dwellings-around-the-alps-fr", + "1363-IT": "whs-prehistoric-pile-dwellings-around-the-alps-it", + "1363-SI": "whs-prehistoric-pile-dwellings-around-the-alps-si", + "1364-BH": "whs-pearling-testimony-of-an-island-economy", + "1366-TR": "whs-selimiye-mosque-and-its-social-complex", + "1367-PT": "whs-garrison-border-town-of-elvas-and-its-fortifications", + "1368-DE": "whs-fagus-factory-in-alfeld", + "1369-AU": "whs-ningaloo-coast", + "1370-IL": "whs-caves-of-maresha-and-bet-guvrin-in-the-judean-lowlands-as-a-microcosm-of-the-land-of-the-caves", + "1371-ES": "whs-cultural-landscape-of-the-serra-de-tramuntana", + "1372-IR": "whs-the-persian-garden", + "1375-IN": "whs-santiniketan", + "1376-BB": "whs-historic-bridgetown-and-its-garrison", + "1377-JO": "whs-wadi-rum-protected-area", + "1379-DE": "whs-margravial-opera-house-bayreuth", + "1380-CF": "whs-sangha-trinational-cf", + "1380-CG": "whs-sangha-trinational-cg", + "1380-CM": "whs-sangha-trinational-cm", + "1382-MN": "whs-petroglyphic-complexes-of-the-mongolian-altai", + "1386-PW": "whs-rock-islands-southern-lagoon", + "1387-PT": "whs-university-of-coimbra-alta-and-sofia", + "1388-CN": "whs-chengjiang-fossil-site", + "1389-CN": "whs-site-of-xanadu", + "1390-IT": "whs-vineyard-landscape-of-piedmont-langhe-roero-and-monferrato", + "1393-IL": "whs-sites-of-human-evolution-at-mount-carmel-the-nahal-me-arot-wadi-el-mughara-caves", + "1396-MY": "whs-archaeological-heritage-of-the-lenggong-valley", + "1397-IR": "whs-masjed-e-jame-of-isfahan", + "1398-IR": "whs-gonbad-e-qabus", + "1399-FJ": "whs-levuka-historical-port-town", + "1400-TD": "whs-lakes-of-ounianga", + "1401-MA": "whs-rabat-modern-capital-and-historic-city-a-shared-heritage", + "1402-QA": "whs-al-zubarah-archaeological-site", + "1403-PH": "whs-mount-hamiguitan-range-wildlife-sanctuary", + "1404-CA": "whs-landscape-of-grand-pre", + "1405-TR": "whs-neolithic-site-of-catalhoyuk", + "1406-IN": "whs-great-himalayan-national-park-conservation-area", + "1407-SN": "whs-bassari-country-bassari-fula-and-bedik-cultural-landscapes", + "1410-MX": "whs-el-pinacate-and-gran-desierto-de-altar-biosphere-reserve", + "1411-UA": "whs-ancient-city-of-tauric-chersonese-and-its-chora", + "1412-CA": "whs-red-bay-basque-whaling-station", + "1413-DE": "whs-bergpark-wilhelmshohe", + "1414-CN": "whs-xinjiang-tianshan", + "1415-CA": "whs-pimachiowin-aki", + "1416-DK": "whs-stevns-klint", + "1418-JP": "whs-fujisan-sacred-place-and-source-of-artistic-inspiration", + "1422-IR": "whs-golestan-palace", + "1423-IR": "whs-cultural-landscape-of-maymand", + "1424-PL": "whs-wooden-tserkvas-of-the-carpathian-region-in-poland-and-ukraine-pl", + "1424-UA": "whs-wooden-tserkvas-of-the-carpathian-region-in-poland-and-ukraine-ua", + "1425-FR": "whs-the-climats-terroirs-of-burgundy", + "1426-FR": "whs-decorated-cave-of-pont-d-arc-known-as-grotte-chauvet-pont-d-arc-ardeche", + "1427-IT": "whs-mount-etna", + "1430-NA": "whs-namib-sand-sea", + "1431-GW": "whs-coastal-and-marine-ecosystems-of-the-bijagos-archipelago-omati-minho", + "1432-BW": "whs-okavango-delta", + "1433-PS": "whs-birthplace-of-jesus-church-of-the-nativity-and-the-pilgrimage-route-bethlehem", + "1434-FR": "whs-chaine-des-puys-limagne-fault-tectonic-arena", + "1435-US": "whs-monumental-earthworks-of-poverty-point", + "1437-IQ": "whs-erbil-citadel", + "1438-VN": "whs-trang-an-landscape-complex", + "1439-KR": "whs-namhansanseong", + "1440-MN": "whs-great-burkhan-khaldun-mountain-and-its-surrounding-sacred-landscape", + "1441-NL": "whs-van-nellefabriek", + "1442-CN": "whs-silk-roads-the-routes-network-of-chang-an-tianshan-corridor-cn", + "1442-KG": "whs-silk-roads-the-routes-network-of-chang-an-tianshan-corridor-kg", + "1442-KZ": "whs-silk-roads-the-routes-network-of-chang-an-tianshan-corridor-kz", + "1443-CN": "whs-the-grand-canal", + "1444-MM": "whs-pyu-ancient-cities", + "1446-JO": "whs-baptism-site-bethany-beyond-the-jordan-al-maghtas", + "1447-DE": "whs-carolingian-westwork-and-civitas-corvey", + "1448-MN": "whs-landscapes-of-dauria-mn", + "1448-RU": "whs-landscapes-of-dauria-ru", + "1449-JP": "whs-tomioka-silk-mill-and-related-sites", + "1450-KE": "whs-thimlich-ohinga-archaeological-site", + "1452-TR": "whs-bursa-and-cumal-k-z-k-the-birth-of-the-ottoman-empire", + "1453-CR": "whs-precolumbian-chiefdom-settlements-with-stone-spheres-of-the-diquis", + "1455-IR": "whs-susa", + "1456-IR": "whs-shahr-i-sokhta", + "1457-TR": "whs-pergamon-and-its-multi-layered-cultural-landscape", + "1459-AR": "whs-qhapaq-nan-andean-road-system-ar", + "1459-BO": "whs-qhapaq-nan-andean-road-system-bo", + "1459-CL": "whs-qhapaq-nan-andean-road-system-cl", + "1459-CO": "whs-qhapaq-nan-andean-road-system-co", + "1459-EC": "whs-qhapaq-nan-andean-road-system-ec", + "1459-PE": "whs-qhapaq-nan-andean-road-system-pe", + "1461-TH": "whs-kaeng-krachan-forest-complex", + "1463-MX": "whs-aqueduct-of-padre-tembleque-hydraulic-system", + "1464-UY": "whs-fray-bentos-industrial-landscape", + "1465-FR": "whs-champagne-hillsides-houses-and-cellars", + "1466-US": "whs-san-antonio-missions", + "1467-DE": "whs-speicherstadt-and-kontorhaus-district-with-chilehaus", + "1468-DE": "whs-moravian-church-settlements-de", + "1468-DK": "whs-moravian-church-settlements-dk", + "1468-GB": "whs-moravian-church-settlements-gb", + "1468-US": "whs-moravian-church-settlements-us", + "1469-DK": "whs-the-par-force-hunting-landscape-in-north-zealand", + "1470-DE": "whs-naumburg-cathedral", + "1471-IL": "whs-necropolis-of-bet-she-arim-a-landmark-of-jewish-renewal", + "1472-SA": "whs-rock-art-in-the-hail-region-of-saudi-arabia", + "1473-RO": "whs-brancusi-monumental-ensemble-of-targu-jiu", + "1474-CN": "whs-tusi-sites", + "1475-TD": "whs-ennedi-massif-natural-and-cultural-landscape", + "1477-KR": "whs-baekje-historic-areas", + "1478-CZ": "whs-erzgebirge-krusnohori-mining-region-cz", + "1478-DE": "whs-erzgebirge-krusnohori-mining-region-de", + "1480-IN": "whs-victorian-gothic-and-art-deco-ensembles-of-mumbai", + "1481-IQ": "whs-the-ahwar-of-southern-iraq-refuge-of-biodiversity-and-the-relict-landscape-of-the-mesopotamian-cities", + "1483-SG": "whs-singapore-botanic-gardens", + "1484-JP": "whs-sites-of-japan-s-meiji-industrial-revolution-iron-and-steel-shipbuilding-and-coal-mining", + "1485-GB": "whs-the-forth-bridge", + "1486-NO": "whs-rjukan-notodden-industrial-heritage-site", + "1487-IT": "whs-arab-norman-palermo-and-the-cathedral-churches-of-cefalu-and-monreale", + "1488-TR": "whs-diyarbak-r-fortress-and-hevsel-gardens-cultural-landscape", + "1490-KG": "whs-western-tien-shan-kg", + "1490-KZ": "whs-western-tien-shan-kz", + "1490-UZ": "whs-western-tien-shan-uz", + "1492-PS": "whs-palestine-land-of-olives-and-vines-cultural-landscape-of-southern-jerusalem-battir", + "1493-BR": "whs-pampulha-modern-ensemble", + "1495-JP": "whs-hidden-christian-sites-in-the-nagasaki-region", + "1496-US": "whs-the-20th-century-architecture-of-frank-lloyd-wright", + "1497-CA": "whs-mistaken-point", + "1498-KR": "whs-seowon-korean-neo-confucian-academies", + "1499-AG": "whs-antigua-naval-dockyard-and-related-archaeological-sites", + "1500-GB": "whs-gorham-s-cave-complex", + "1501-ES": "whs-antequera-dolmens-site", + "1502-IN": "whs-archaeological-site-of-nalanda-mahavihara-at-nalanda-bihar", + "1503-FM": "whs-nan-madol-ceremonial-centre-of-eastern-micronesia", + "1504-BA": "whs-stecci-medieval-tombstone-graveyards-ba", + "1504-HR": "whs-stecci-medieval-tombstone-graveyards-hr", + "1504-ME": "whs-stecci-medieval-tombstone-graveyards-me", + "1504-RS": "whs-stecci-medieval-tombstone-graveyards-rs", + "1505-IR": "whs-lut-desert", + "1506-IR": "whs-the-persian-qanat", + "1507-TH": "whs-phu-phrabat-a-testimony-to-the-sima-stone-tradition-of-the-dvaravati-period", + "1508-CN": "whs-zuojiang-huashan-rock-art-cultural-landscape", + "1509-CN": "whs-hubei-shennongjia", + "1510-MX": "whs-archipielago-de-revillagigedo", + "1511-AO": "whs-mbanza-kongo-vestiges-of-the-capital-of-the-former-kingdom-of-kongo", + "1513-IN": "whs-khangchendzonga-national-park", + "1517-GR": "whs-archaeological-site-of-philippi", + "1518-TR": "whs-archaeological-site-of-ani", + "1519-TR": "whs-aphrodisias", + "1523-RU": "whs-churches-of-the-pskov-school-of-architecture", + "1525-RU": "whs-assumption-cathedral-and-monastery-of-the-town-island-of-sviyazhsk", + "1526-AR": "whs-los-alerces-national-park", + "1527-DE": "whs-caves-and-ice-age-art-in-the-swabian-jura", + "1528-ES": "whs-talayotic-menorca", + "1529-FR": "whs-taputapuatea", + "1532-KH": "whs-temple-zone-of-sambor-prei-kuk-archaeological-site-of-ancient-ishanapura", + "1533-HR": "whs-venetian-works-of-defence-between-the-16th-and-17th-centuries-stato-da-terra-western-stato-da-mar-hr", + "1533-IT": "whs-venetian-works-of-defence-between-the-16th-and-17th-centuries-stato-da-terra-western-stato-da-mar-it", + "1533-ME": "whs-venetian-works-of-defence-between-the-16th-and-17th-centuries-stato-da-terra-western-stato-da-mar-me", + "1534-MX": "whs-tehuacan-cuicatlan-valley-originary-habitat-of-mesoamerica", + "1535-JP": "whs-sacred-island-of-okinoshima-and-associated-sites-in-the-munakata-region", + "1536-DK": "whs-kujataa-greenland-norse-and-inuit-farming-at-the-edge-of-the-ice-cap", + "1537-OM": "whs-ancient-city-of-qalhat", + "1538-IT": "whs-ivrea-industrial-city-of-the-20th-century", + "1539-PL": "whs-tarnowskie-gory-lead-silver-zinc-mine-and-its-underground-water-management-system", + "1540-CN": "whs-qinghai-hoh-xil", + "1541-CN": "whs-kulangsu-a-historic-international-settlement", + "1542-BH": "whs-dilmun-burial-mounds", + "1544-IR": "whs-historic-city-of-yazd", + "1545-ZA": "whs-khomani-cultural-landscape", + "1548-BR": "whs-valongo-wharf-archaeological-site", + "1549-AZ": "whs-historic-centre-of-sheki-with-the-khan-s-palace", + "1550-ER": "whs-asmara-a-modernist-african-city", + "1551-IN": "whs-historic-city-of-ahmadabad", + "1552-RO": "whs-rosia-montana-mining-landscape", + "1553-DE": "whs-archaeological-border-complex-of-hedeby-and-the-danevirke", + "1555-BE": "whs-colonies-of-benevolence-be", + "1555-NL": "whs-colonies-of-benevolence-nl", + "1557-DK": "whs-aasivissuit-nipisat-inuit-hunting-ground-between-ice-and-sea", + "1558-CZ": "whs-zatec-and-the-landscape-of-saaz-hops", + "1559-CN": "whs-fanjingshan", + "1560-ES": "whs-caliphate-city-of-medina-azahara", + "1561-CN": "whs-quanzhou-emporium-of-the-world-in-song-yuan-china", + "1562-KR": "whs-sansa-buddhist-mountain-monasteries-in-korea", + "1563-SA": "whs-al-ahsa-oasis-an-evolving-cultural-landscape", + "1564-CA": "whs-tr-ondek-klondike", + "1565-PS": "whs-hebron-al-khalil-old-town", + "1567-BE": "whs-funerary-and-memory-sites-of-the-first-world-war-western-front-be", + "1567-FR": "whs-funerary-and-memory-sites-of-the-first-world-war-western-front-fr", + "1568-IR": "whs-sassanid-archaeological-landscape-of-fars-region", + "1569-FR": "whs-the-maison-carree-of-nimes", + "1570-IN": "whs-kakatiya-rudreshwara-ramappa-temple-telangana", + "1571-IT": "whs-le-colline-del-prosecco-di-conegliano-e-valdobbiadene", + "1572-TR": "whs-gobekli-tepe", + "1573-PT": "whs-royal-building-of-mafra-palace-basilica-convent-cerco-garden-and-hunting-park-tapada", + "1574-JP": "whs-amami-oshima-island-tokunoshima-island-northern-part-of-okinawa-island-and-iriomote-island", + "1575-ZA": "whs-barberton-makhonjwa-mountains", + "1577-AU": "whs-budj-bim-cultural-landscape", + "1578-ES": "whs-risco-caido-and-the-sacred-mountains-of-gran-canaria-cultural-landscape", + "1580-DE": "whs-water-management-system-of-augsburg", + "1582-PA": "whs-the-colonial-transisthmian-route-of-panama", + "1584-AZ": "whs-hyrcanian-forests-az", + "1584-IR": "whs-hyrcanian-forests-ir", + "1585-IR": "whs-trans-iranian-railway", + "1586-RW": "whs-memorial-sites-of-the-genocide-nyamata-murambi-gisozi-and-bisesero", + "1587-LA": "whs-megalithic-jar-sites-in-xiengkhuang-plain-of-jars", + "1588-MM": "whs-bagan", + "1589-CZ": "whs-landscape-for-breeding-and-training-of-ceremonial-carriage-horses-at-kladruby-nad-labem", + "1590-PT": "whs-sanctuary-of-bom-jesus-do-monte-in-braga", + "1591-KR": "whs-getbol-korean-tidal-flats", + "1592-CN": "whs-archaeological-ruins-of-liangzhu-city", + "1593-JP": "whs-mozu-furuichi-kofun-group-mounded-tombs-of-ancient-japan", + "1594-GB": "whs-jodrell-bank-observatory", + "1595-JM": "whs-the-archaeological-ensemble-of-17th-century-port-royal", + "1597-CA": "whs-writing-on-stone-aisinai-pi", + "1599-PL": "whs-krzemionki-prehistoric-striped-flint-mining-region", + "1602-BF": "whs-ancient-ferrous-metallurgy-sites-of-burkina-faso", + "1603-FR": "whs-french-austral-lands-and-seas", + "1604-IS": "whs-vatnajokull-national-park-dynamic-nature-of-fire-and-ice", + "1605-IN": "whs-jaipur-city-rajasthan", + "1606-CN": "whs-migratory-bird-sanctuaries-along-the-coast-of-yellow-sea-bohai-gulf-of-china", + "1608-AT": "whs-frontiers-of-the-roman-empire-the-danube-limes-western-segment-at", + "1608-DE": "whs-frontiers-of-the-roman-empire-the-danube-limes-western-segment-de", + "1608-SK": "whs-frontiers-of-the-roman-empire-the-danube-limes-western-segment-sk", + "1610-ID": "whs-ombilin-coal-mining-heritage-of-sawahlunto", + "1611-BR": "whs-lencois-maranhenses-national-park", + "1612-UY": "whs-the-work-of-engineer-eladio-dieste-church-of-atlantida", + "1613-AT": "whs-the-great-spa-towns-of-europe-at", + "1613-BE": "whs-the-great-spa-towns-of-europe-be", + "1613-CZ": "whs-the-great-spa-towns-of-europe-cz", + "1613-DE": "whs-the-great-spa-towns-of-europe-de", + "1613-FR": "whs-the-great-spa-towns-of-europe-fr", + "1613-GB": "whs-the-great-spa-towns-of-europe-gb", + "1613-IT": "whs-the-great-spa-towns-of-europe-it", + "1614-DE": "whs-mathildenhohe-darmstadt", + "1616-GE": "whs-colchic-rainforests-and-wetlands", + "1618-ES": "whs-paseo-del-prado-and-buen-retiro-a-landscape-of-arts-and-sciences", + "1619-SA": "whs-hima-cultural-area", + "1620-BR": "whs-sitio-roberto-burle-marx", + "1621-MN": "whs-deer-stone-monuments-and-related-bronze-age-sites", + "1622-TR": "whs-arslantepe-mound", + "1623-IT": "whs-padua-s-fourteenth-century-fresco-cycles", + "1624-PE": "whs-chankillo-archaeoastronomical-complex", + "1625-FR": "whs-cordouan-lighthouse", + "1627-TJ": "whs-cultural-heritage-sites-of-ancient-khuttal", + "1631-DE": "whs-frontiers-of-the-roman-empire-the-lower-german-limes-de", + "1631-NL": "whs-frontiers-of-the-roman-empire-the-lower-german-limes-nl", + "1632-JP": "whs-jomon-prehistoric-sites-in-northern-japan", + "1633-GB": "whs-the-slate-landscape-of-northwest-wales", + "1634-CL": "whs-settlement-and-artificial-mummification-of-the-chinchorro-culture-in-the-arica-and-parinacota-region", + "1635-FR": "whs-nice-winter-resort-town-of-the-riviera", + "1636-DE": "whs-shum-sites-of-speyer-worms-and-mainz", + "1638-CN": "whs-badain-jaran-desert-towers-of-sand-and-lakes", + "1640-TN": "whs-djerba-testimony-to-a-settlement-pattern-in-an-island-territory", + "1641-ET": "whs-the-gedeo-cultural-landscape", + "1642-KP": "whs-mount-kumgang-diamond-mountain-from-the-sea", + "1643-SI": "whs-the-works-of-joze-plecnik-in-ljubljana-human-centred-urban-design", + "1645-IN": "whs-dholavira-a-harappan-city", + "1647-IR": "whs-cultural-landscape-of-hawraman-uramanat", + "1648-CI": "whs-sudanese-style-mosques-in-northern-cote-d-ivoire", + "1650-IT": "whs-the-porticoes-of-bologna", + "1653-GA": "whs-ivindo-national-park", + "1654-RU": "whs-petroglyphs-of-lake-onega-and-the-white-sea", + "1656-DE": "whs-jewish-medieval-heritage-of-erfurt", + "1657-FR": "whs-volcanoes-and-forests-of-mount-pelee-and-the-pitons-of-northern-martinique", + "1658-LV": "whs-old-town-of-kuldiga", + "1660-DK": "whs-viking-age-ring-fortresses", + "1661-LT": "whs-modernist-kaunas-architecture-of-optimism-1919-1939", + "1662-TH": "whs-the-ancient-town-of-si-thep-and-its-associated-dvaravati-monuments", + "1663-GT": "whs-national-archaeological-park-tak-alik-ab-aj", + "1665-CN": "whs-cultural-landscape-of-old-tea-forests-of-the-jingmai-mountain-in-pu-er", + "1666-KR": "whs-gaya-tumuli", + "1667-KH": "whs-koh-ker-archaeological-site-of-ancient-lingapura-or-chok-gargyar", + "1668-IR": "whs-the-persian-caravanserai", + "1669-TR": "whs-gordion", + "1670-IN": "whs-sacred-ensembles-of-the-hoysalas", + "1671-ID": "whs-the-cosmological-axis-of-yogyakarta-and-its-historic-landmarks", + "1673-BA": "whs-vjetrenica-cave-ravno", + "1675-TJ": "whs-silk-roads-zarafshan-karakum-corridor-tj", + "1675-TM": "whs-silk-roads-zarafshan-karakum-corridor-tm", + "1675-UZ": "whs-silk-roads-zarafshan-karakum-corridor-uz", + "1676-ZA": "whs-human-rights-liberation-and-reconciliation-nelson-mandela-legacy-sites", + "1678-RU": "whs-astronomical-observatories-of-kazan-federal-university", + "1680-SR": "whs-jodensavanne-archaeological-site-jodensavanne-settlement-and-cassipora-creek-cemetery", + "1681-AR": "whs-esma-museum-and-site-of-memory-former-clandestine-centre-of-detention-torture-and-extermination", + "1683-NL": "whs-eisinga-planetarium-in-franeker", + "1685-TJ": "whs-tugay-forests-of-the-tigrovaya-balka-nature-reserve", + "1686-CA": "whs-anticosti", + "1687-PS": "whs-ancient-jericho-tell-es-sultan", + "1688-RU": "whs-cultural-landscape-of-kenozero-lake", + "1689-US": "whs-hopewell-ceremonial-earthworks", + "1692-IT": "whs-evaporitic-karst-and-caves-of-northern-apennines", + "1693-KZ": "whs-cold-winter-deserts-of-turan-kz", + "1693-TM": "whs-cold-winter-deserts-of-turan-tm", + "1693-UZ": "whs-cold-winter-deserts-of-turan-uz", + "1694-TR": "whs-wooden-hypostyle-mosques-of-medieval-anatolia", + "1695-GR": "whs-zagori-cultural-landscape", + "1696-AZ": "whs-cultural-landscape-of-khinalig-people-and-koc-yolu-transhumance-route", + "1697-RW": "whs-nyungwe-national-park", + "1698-JP": "whs-sado-island-gold-mines", + "1699-SA": "whs-uruq-bani-ma-arid", + "1700-YE": "whs-landmarks-of-the-ancient-kingdom-of-saba-marib", + "1702-LB": "whs-rachid-karami-international-fair-tripoli", + "1703-UA": "whs-the-historic-centre-of-odesa", + "1704-MX": "whs-wixarika-route-through-sacred-sites-to-wirikuta-tatehuari-huajuye", + "1705-DE": "whs-schwerin-residence-ensemble", + "1707-FR": "whs-te-henua-enata-the-marquesas-islands", + "1708-IT": "whs-via-appia-regina-viarum", + "1709-AU": "whs-murujuga-cultural-landscape", + "1711-IN": "whs-moidams-the-mound-burial-system-of-the-ahom-dynasty", + "1712-SA": "whs-the-cultural-landscape-of-al-faw-archaeological-area", + "1713-BF": "whs-royal-court-of-tiebele", + "1714-CN": "whs-beijing-central-axis-a-building-ensemble-exhibiting-the-ideal-order-of-the-chinese-capital", + "1716-IR": "whs-hegmataneh", + "1718-RO": "whs-frontiers-of-the-roman-empire-dacia", + "1720-KE": "whs-the-historic-town-and-archaeological-site-of-gedi", + "1721-JO": "whs-umm-al-jimal", + "1722-GB": "whs-the-flow-country", + "1723-ZA": "whs-the-emergence-of-modern-human-behaviour-the-pleistocene-occupation-sites-of-south-africa", + "1725-FR": "whs-megaliths-of-carnac-and-of-the-shores-of-morbihan", + "1726-DE": "whs-the-palaces-of-king-ludwig-ii-of-bavaria-neuschwanstein-linderhof-schachen-and-herrenchiemsee", + "1728-DK": "whs-mons-klint", + "1730-IT": "whs-funerary-tradition-in-the-prehistory-of-sardinia-the-domus-de-janas", + "1731-TR": "whs-sardis-and-the-lydian-tumuli-of-bin-tepe", + "1732-VN": "whs-yen-tu-vinh-nghiem-con-son-kiep-bac-complex-of-monuments-and-landscapes", + "1733-GR": "whs-minoan-palatial-centres", + "1734-MY": "whs-forest-research-institute-malaysia-forest-park-selangor", + "1735-AE": "whs-faya-palaeolandscape", + "1736-CN": "whs-xixia-imperial-tombs", + "1739-IN": "whs-maratha-military-landscapes-of-india", + "1740-KR": "whs-petroglyphs-along-the-bangucheon-stream", + "1743-RU": "whs-rock-paintings-of-shulgan-tash-cave", + "1744-IR": "whs-prehistoric-sites-of-the-khorramabad-valley", + "1745-CM": "whs-diy-gid-biy-cultural-landscape-of-the-mandara-mountains", + "1746-SL": "whs-gola-tiwai-complex", + "1747-BR": "whs-peruacu-river-canyon", + "1748-KH": "whs-cambodian-memorial-sites-from-centres-of-repression-to-places-of-peace-and-reflection", + "1749-PS": "whs-saint-hilarion-monastery-tell-umm-amer" +} diff --git a/data/reference/sources.lock.json b/data/reference/sources.lock.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/data/reference/sources.lock.json @@ -0,0 +1 @@ +{} diff --git a/data/reference/whc-sites-2025.json.gz b/data/reference/whc-sites-2025.json.gz deleted file mode 100644 index 6d08097..0000000 Binary files a/data/reference/whc-sites-2025.json.gz and /dev/null differ diff --git a/data/reference/whc-sites.json.gz b/data/reference/whc-sites.json.gz new file mode 100644 index 0000000..0708441 Binary files /dev/null and b/data/reference/whc-sites.json.gz differ diff --git a/package-lock.json b/package-lock.json index cbb6b04..0831b4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,8 @@ "license": "AGPL-3.0-only", "dependencies": { "airport-data": "^1.0.1", - "all-the-cities": "^3.1.0", "country-state-city": "^3.2.1", - "i18n-iso-countries": "^7.14.0", - "world-countries": "^5.1.0" + "i18n-iso-countries": "^7.14.0" }, "devDependencies": { "@types/node": "^22.10.0", @@ -974,15 +972,6 @@ "integrity": "sha512-Y3k2q5I/CwLKW1mul7ECRQQSEQpBm5jebmgutjO/DLbnBp6CG7jaKD58uu8O8HS0oip23TurO6czXBh5Cet9zg==", "license": "Unlicense" }, - "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==", - "license": "MIT", - "dependencies": { - "pbf": "^3.2.1" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1166,26 +1155,6 @@ "node": ">= 12" } }, - "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" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -1246,19 +1215,6 @@ "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==", - "license": "BSD-3-Clause", - "dependencies": { - "ieee754": "^1.1.12", - "resolve-protobuf-schema": "^2.1.0" - }, - "bin": { - "pbf": "bin/pbf" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1295,21 +1251,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/protocol-buffers-schema": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", - "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", - "license": "MIT" - }, - "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==", - "license": "MIT", - "dependencies": { - "protocol-buffers-schema": "^3.3.1" - } - }, "node_modules/rollup": { "version": "4.64.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.5.tgz", @@ -2066,11 +2007,6 @@ "engines": { "node": ">=8" } - }, - "node_modules/world-countries": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/world-countries/-/world-countries-5.1.0.tgz", - "integrity": "sha512-CXR6EBvTbArDlDDIWU3gfKb7Qk0ck2WNZ234b/A0vuecPzIfzzxH+O6Ejnvg1sT8XuiZjVlzOH0h08ZtaO7g0w==" } } } diff --git a/package.json b/package.json index a4145ab..e30a970 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "test": "vitest run", "fetch": "tsx scripts/fetch.ts", "build": "tsx scripts/build.ts", - "city-admin1": "tsx scripts/city-admin1.ts", - "check": "npm run typecheck && npm test" + "check": "npm run typecheck && npm test", + "refresh": "tsx scripts/refresh.ts" }, "devDependencies": { "@types/node": "^22.10.0", @@ -21,9 +21,7 @@ }, "dependencies": { "airport-data": "^1.0.1", - "all-the-cities": "^3.1.0", "country-state-city": "^3.2.1", - "i18n-iso-countries": "^7.14.0", - "world-countries": "^5.1.0" + "i18n-iso-countries": "^7.14.0" } } diff --git a/scripts/city-admin1.ts b/scripts/city-admin1.ts deleted file mode 100644 index e52edc0..0000000 --- a/scripts/city-admin1.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Vendor GeoNames' current admin-1 code for every city all-the-cities lists, so the cities - * and regions datasets take codes and names from one GeoNames day (src/reference/cities.ts). - * Run it with admin1CodesASCII.txt from the same day, gzipped into data/reference/. - * - * curl -LO https://download.geonames.org/export/dump/cities500.zip && unzip cities500.zip - * curl -L https://download.geonames.org/export/dump/admin1CodesASCII.txt | gzip -9 > data/reference/admin1CodesASCII.txt.gz - * npm run city-admin1 -- cities500.txt - * - * Writes data/reference/city-admin1.tsv.gz: one "\t." line per city - * cities500 still lists (CC BY 4.0, GeoNames). - */ -import { createRequire } from "node:module"; -import { readFileSync, writeFileSync } from "node:fs"; -import { gzipSync } from "node:zlib"; -import { join } from "node:path"; -import { REFERENCE_DIR } from "../src/reference/common"; - -const require = createRequire(import.meta.url); -const allCities = require("all-the-cities") as { cityId: number }[]; - -const source = process.argv[2]; -if (!source) { - console.error("usage: npm run city-admin1 -- "); - process.exit(1); -} - -const wanted = new Set(allCities.map((c) => String(c.cityId))); -const rows: [number, string][] = []; -for (const line of readFileSync(source, "utf8").split("\n")) { - const f = line.split("\t"); - if (f.length < 11 || !wanted.has(f[0]!)) continue; - rows.push([Number(f[0]), `${f[0]}\t${f[8]}.${f[10]}`]); -} -rows.sort((a, b) => a[0] - b[0]); -writeFileSync(join(REFERENCE_DIR, "city-admin1.tsv.gz"), gzipSync(rows.map((r) => r[1]).join("\n") + "\n", { level: 9 })); -console.log(`cities with a current admin-1 code: ${rows.length} of ${wanted.size}`); diff --git a/scripts/refresh.ts b/scripts/refresh.ts new file mode 100644 index 0000000..e85ad8e --- /dev/null +++ b/scripts/refresh.ts @@ -0,0 +1,119 @@ +/** + * Look for a newer version of each saved source in data/reference/ and update the saved copy + * when the part the build uses changed. One conditional request per source, no retries; + * GeoNames only in the first week of the month (its files change daily), unless `--all`. + * Anything that fails keeps the saved copy, and the script still exits 0: a refresh is a + * comfort, never a reason for the weekly build to stop. + * Run: npm run refresh [-- --all] + */ +import { existsSync, readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { gunzipSync, gzipSync } from "node:zlib"; +import { REFERENCE_DIR } from "../src/reference/common"; +import { + admin1Codes, + citiesTsv, + countriesJson, + COUNTRIES_URL, + GEONAMES, + get, + type Seen, + UNESCO_EXPORT_URL, + UNESCO_META_URL, + unescoSites, +} from "../src/reference/refresh"; + +const LOCK = join(REFERENCE_DIR, "sources.lock.json"); +const lock = (existsSync(LOCK) ? JSON.parse(readFileSync(LOCK, "utf8")) : {}) as Record; +const all = process.argv.includes("--all"); +const report: string[] = []; + +const gunzipped = (file: string) => gunzipSync(readFileSync(join(REFERENCE_DIR, file))).toString("utf8"); +/** Writes a saved copy only when its text changed, so an unchanged source leaves no diff. */ +function save(file: string, text: string): boolean { + const path = join(REFERENCE_DIR, file); + const gz = file.endsWith(".gz"); + if (existsSync(path) && (gz ? gunzipped(file) : readFileSync(path, "utf8")) === text) return false; + writeFileSync(path, gz ? gzipSync(text, { level: 9 }) : text); + return true; +} + +async function step(name: string, run: () => Promise) { + try { + report.push(`${name}: ${await run()}`); + } catch (err) { + const cause = err instanceof Error && err.cause instanceof Error ? `: ${err.cause.message}` : ""; + report.push(`${name}: kept the saved copy (${err instanceof Error ? err.message : String(err)}${cause})`); + } +} + +await step("GeoNames cities and regions", async () => { + if (!all && new Date().getUTCDate() > 7) return "checked in the first week of the month"; + const citiesUrl = `${GEONAMES}cities1000.zip`; + const admin1Url = `${GEONAMES}admin1CodesASCII.txt`; + const cities = await get(citiesUrl, lock["geonames-cities"]); + const admin1 = await get(admin1Url, lock["geonames-admin1"], 30_000); + if (!cities && !admin1) return "not modified"; + + // Both files are read from the same day, or neither is saved. + const saved = gunzipped("cities.tsv.gz").split("\n").filter(Boolean).map((l) => l.split("\t")); + let rows = saved; + if (cities) { + const dir = mkdtempSync(join(tmpdir(), "geonames-")); + try { + writeFileSync(join(dir, "cities1000.zip"), cities.body); + const text = execFileSync("unzip", ["-p", join(dir, "cities1000.zip"), "cities1000.txt"], { maxBuffer: 1 << 28 }).toString("utf8"); + rows = citiesTsv(text, saved); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + } + const savedAdmin1 = gunzipped("admin1CodesASCII.txt.gz").split("\n").filter(Boolean).length; + const codes = admin1 ? admin1Codes(admin1.body.toString("utf8"), savedAdmin1) : null; + + const changed = [ + save("cities.tsv.gz", rows.map((r) => r.join("\t")).join("\n") + "\n") && `${rows.length} cities`, + codes !== null && save("admin1CodesASCII.txt.gz", codes) && "region names", + ].filter(Boolean); + if (cities) lock["geonames-cities"] = cities.seen; + if (admin1) lock["geonames-admin1"] = admin1.seen; + return changed.length ? `updated (${changed.join(", ")})` : "downloaded, nothing used here changed"; +}); + +await step("Countries", async () => { + const res = await get(COUNTRIES_URL, lock.countries, 30_000); + if (!res) return "not modified"; + const saved = (JSON.parse(readFileSync(join(REFERENCE_DIR, "countries.json"), "utf8")) as unknown[]).length; + const countries = countriesJson(res.body.toString("utf8"), saved); + const changed = save("countries.json", JSON.stringify(countries) + "\n"); + lock.countries = res.seen; + return changed ? `updated (${countries.length} countries)` : "downloaded, nothing used here changed"; +}); + +await step("UNESCO World Heritage List", async () => { + // The dataset's own "modified" date first: the list itself is fetched only when it moved. + const meta = await get(UNESCO_META_URL, undefined, 30_000); + const info = JSON.parse(meta!.body.toString("utf8")) as { metas?: { default?: { modified?: string; data_processed?: string } } }; + const modified = info.metas?.default?.modified ?? info.metas?.default?.data_processed; + if (!modified) throw new Error("no modified date in the dataset's metadata"); + if (lock.unesco?.modified === modified) return "not modified"; + const res = await get(UNESCO_EXPORT_URL, lock.unesco); + if (!res) { + lock.unesco = { ...lock.unesco!, modified }; + return "not modified"; + } + const saved = (JSON.parse(gunzipped("whc-sites.json.gz")) as unknown[]).length; + const known = JSON.parse(readFileSync(join(REFERENCE_DIR, "heritage-ids.json"), "utf8")) as Record; + const { sites, ids } = unescoSites(res.body.toString("utf8"), saved, known); + const changed = save("whc-sites.json.gz", JSON.stringify(sites) + "\n"); + save("heritage-ids.json", JSON.stringify(ids, null, 2) + "\n"); + lock.unesco = { ...res.seen, modified }; + return changed ? `updated (${sites.length} sites)` : "downloaded, nothing used here changed"; +}); + +writeFileSync(LOCK, JSON.stringify(lock, null, 2) + "\n"); +const summary = ["Source refresh", "", ...report.map((r) => `- ${r}`)].join("\n"); +console.log(summary); +if (process.env.GITHUB_STEP_SUMMARY) writeFileSync(process.env.GITHUB_STEP_SUMMARY, summary + "\n", { flag: "a" }); diff --git a/src/catalog.ts b/src/catalog.ts index e134e45..c76ad22 100644 --- a/src/catalog.ts +++ b/src/catalog.ts @@ -61,7 +61,7 @@ export const DATASETS: Dataset[] = [ { id: "cities", name: "Cities worldwide", - description: "Every city GeoNames lists in the all-the-cities package (about 135,000), with population, coordinates and first-level region.", + description: "Every city of 1,000 people or more GeoNames lists (about 135,000), with population, coordinates and first-level region.", license: "CC-BY-4.0", sources: ["geonames"], schedule: "weekly", @@ -94,7 +94,7 @@ export const DATASETS: Dataset[] = [ { id: "heritage", name: "UNESCO World Heritage sites", - description: "Every World Heritage site through the 2025 session, once per country it is in.", + description: "Every World Heritage site, once per country it is in.", license: "CC-BY-SA-3.0-IGO", sources: ["unesco-whc"], schedule: "weekly", diff --git a/src/reference/cities.ts b/src/reference/cities.ts index 4412f49..a22221e 100644 --- a/src/reference/cities.ts +++ b/src/reference/cities.ts @@ -1,14 +1,15 @@ /** - * The cities and regions datasets, from GeoNames (CC BY 4.0) by way of the all-the-cities - * package, with each city's admin-1 code and each code's name from one GeoNames day, - * vendored in data/reference/. A code GeoNames doesn't name takes a name from the dr5hn + * The cities and regions datasets, from GeoNames (CC BY 4.0): every city of 1,000 people or + * more with its admin-1 code (data/reference/cities.tsv.gz), and each code's name + * (admin1CodesASCII.txt.gz), both from one GeoNames day and kept current by + * `npm run refresh`. A code GeoNames doesn't name takes a name from the dr5hn * countries-states-cities database (ODbL), matched by nearest centroid. * * cities/top.json the 10,000 most populous cities - * cities/all.json every city all-the-cities lists (about 135,000) + * cities/all.json every city (about 135,000) * regions/regions.json first-level regions, named * - * Moved from Postcards' scripts/build-reference.mjs; the output is the same. + * Moved from Postcards' scripts/build-reference.mjs. */ import { createRequire } from "node:module"; import { readFileSync } from "node:fs"; @@ -18,15 +19,6 @@ import { REFERENCE_DIR, type Built } from "./common"; const require = createRequire(import.meta.url); -interface SourceCity { - cityId: number; - name: string; - country: string; - adminCode?: string; - population: number; - loc: { coordinates: [number, number] }; -} - export interface CityRecord { id: string; name: string; @@ -57,30 +49,21 @@ const FR_REGION_NAMES: Record = { const round4 = (n: number): number => Math.round(n * 1e4) / 1e4; -function tsv(file: string): Map { - return new Map( - gunzipSync(readFileSync(join(REFERENCE_DIR, file))) - .toString("utf8") - .split("\n") - .filter(Boolean) - .map((line) => { - const [key = "", value = ""] = line.split("\t"); - return [key, value]; - }), - ); +function lines(file: string): string[][] { + return gunzipSync(readFileSync(join(REFERENCE_DIR, file))) + .toString("utf8") + .split("\n") + .filter(Boolean) + .map((line) => line.split("\t")); } export function buildCities(): Built { - const allCities = require("all-the-cities") as SourceCity[]; const { State } = require("country-state-city") as { State: { getStatesOfCountry(cc: string): { name: string; latitude: string | null; longitude: string | null }[] }; }; // GeoNames admin-1 names by "CC-code", from admin1CodesASCII.txt. - const admin1Names = new Map([...tsv("admin1CodesASCII.txt.gz")].map(([code, name]) => [code.replace(".", "-"), name])); - // Each city's admin-1 code on the same GeoNames day, by geonameid. all-the-cities carries - // codes from an older snapshot, and GeoNames has since given some to other regions. - const cityAdmin1 = tsv("city-admin1.tsv.gz"); + const admin1Names = new Map(lines("admin1CodesASCII.txt.gz").map(([code = "", name = ""]) => [code.replace(".", "-"), name])); const statesByCountry = new Map(); const statesOf = (cc: string) => { @@ -94,27 +77,27 @@ export function buildCities(): Built { return list; }; - // Cities, each with its current region code where GeoNames still lists it. + // Cities, each with its region code on the same GeoNames day as the names above. A line of + // cities.tsv.gz: geonameid, name, country, admin-1 code, "u" for a city GeoNames no longer + // lists (kept so its id still resolves), population (empty when unknown), latitude, longitude. const cities: (CityRecord & { unlisted?: boolean })[] = []; - const seen = new Set(); - for (const c of allCities) { - if (!c.cityId || seen.has(c.cityId)) continue; - const [lon, lat] = c.loc.coordinates; - if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue; - seen.add(c.cityId); - const current = cityAdmin1.get(String(c.cityId)); - const [cc, admin1] = current ? current.split(".") : []; + const seen = new Set(); + for (const [id = "", name = "", country = "", admin1 = "", unlisted = "", population = "", lat = "", lon = ""] of lines("cities.tsv.gz")) { + const la = Number(lat); + const lo = Number(lon); + if (!id || seen.has(id) || !lat || !lon || !Number.isFinite(la) || !Number.isFinite(lo)) continue; + seen.add(id); cities.push({ - id: String(c.cityId), - name: c.name, - countryIso2: c.country, - subdivisionId: cc === c.country && admin1 ? `${cc}-${admin1}` : null, - lat: round4(lat), - lon: round4(lon), + id, + name, + countryIso2: country, + subdivisionId: admin1 ? `${country}-${admin1}` : null, + lat: round4(la), + lon: round4(lo), // GeoNames uses 0 for "unknown population": null, never a fake 0. - population: c.population > 0 ? c.population : null, + population: Number(population) > 0 ? Number(population) : null, // Only a city GeoNames no longer lists falls back to its nearest neighbour. - unlisted: !current && !!c.adminCode, + unlisted: unlisted === "u", }); } const coded = new Map(); diff --git a/src/reference/countries.ts b/src/reference/countries.ts index 0a7bd2d..a3a87a6 100644 --- a/src/reference/countries.ts +++ b/src/reference/countries.ts @@ -1,22 +1,24 @@ /** - * The countries dataset, from world-countries (ODbL) and ISO 3166-1 codes (public facts, - * by way of i18n-iso-countries, MIT): + * The countries dataset, from mledoze/countries (ODbL; saved at data/reference/countries.json + * and kept current by `npm run refresh`) and ISO 3166-1 codes (public facts, by way of + * i18n-iso-countries, MIT): * * countries/names.json { "RU": "Russia", … } common English name - * countries/article-names.json the same names: the Wikivoyage and Wikipedia article titles + * countries/article-names.json the Wikivoyage and Wikipedia article titles: the same names, but for ARTICLE_TITLES * countries/continents.json { "FR": "Europe", … } * countries/languages.json { "FR": [{ "code": "fra", "name": "French" }], … } * countries/sovereignty.json { "US": "un", "HK": "territory", … } * - * Moved from Postcards' scripts/build-languages.mjs and build-sovereignty.mjs; the output is - * the same. + * Moved from Postcards' scripts/build-languages.mjs and build-sovereignty.mjs. */ import { createRequire } from "node:module"; -import type { Built } from "./common"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { REFERENCE_DIR, type Built } from "./common"; const require = createRequire(import.meta.url); -interface SourceCountry { +export interface SourceCountry { cca2: string; region: string; unMember: boolean; @@ -24,12 +26,16 @@ interface SourceCountry { languages?: Record; } -// Against the UN's own member list (193 members): world-countries 5.1.0 marks the Holy See +// Against the UN's own member list (193 members): mledoze/countries marks the Holy See // a member, but it is a permanent observer (https://www.un.org/en/about-us/member-states). const UN_MEMBER_OVERRIDES: Record = { VA: "territory" }; +// Where the common name isn't the title of the country's Wikivoyage and Wikipedia article: +// both are titled "Republic of the Congo", and "Congo" alone is ambiguous. +const ARTICLE_TITLES: Record = { CG: "Republic of the Congo" }; + export function buildCountries(): Built { - const world = require("world-countries") as SourceCountry[]; + const world = JSON.parse(readFileSync(join(REFERENCE_DIR, "countries.json"), "utf8")) as SourceCountry[]; const iso = require("i18n-iso-countries") as { registerLocale(locale: unknown): void; getNames(lang: string): Record; @@ -48,6 +54,8 @@ export function buildCountries(): Built { } for (const c of [...world].sort((a, b) => a.cca2.localeCompare(b.cca2))) continents[c.cca2] = c.region; + const articleNames = Object.fromEntries(Object.entries(names).map(([cc, name]) => [cc, ARTICLE_TITLES[cc] ?? name])); + const byCca2 = new Map(world.map((c) => [c.cca2, c])); const sovereignty: Record = {}; for (const code of Object.keys(iso.getNames("en"))) { @@ -58,7 +66,7 @@ export function buildCountries(): Built { return { files: [ { path: "v1/countries/names.json", body: names }, - { path: "v1/countries/article-names.json", body: names }, + { path: "v1/countries/article-names.json", body: articleNames }, { path: "v1/countries/continents.json", body: continents }, { path: "v1/countries/languages.json", body: languages }, { path: "v1/countries/sovereignty.json", body: sovereignty }, diff --git a/src/reference/heritage.ts b/src/reference/heritage.ts index 84bad01..ad495a8 100644 --- a/src/reference/heritage.ts +++ b/src/reference/heritage.ts @@ -1,24 +1,27 @@ /** * The heritage dataset: the UNESCO World Heritage List (CC BY-SA 3.0 IGO, © UNESCO World - * Heritage Centre), complete through the 47th session (July 2025). The source payload is - * vendored at data/reference/whc-sites-2025.json.gz, retrieved 2026-07-11 from a public - * mirror of whc.unesco.org's syndication data, so the build needs no network. + * Heritage Centre). The source fields the build reads are saved at + * data/reference/whc-sites.json.gz and kept current by `npm run refresh`; the first copy, + * complete through the 47th session (July 2025), was retrieved 2026-07-11 from a public + * mirror of whc.unesco.org's syndication data. * * heritage/unesco.json { id, name, countryIso2, lat, lon, category } * * A site in several countries is listed once per country, its id suffixed with the country - * code. A site the source has no coordinates for keeps lat 0 / lon 0, read as "no - * coordinates", never guessed. The one site without a country code (Old City of Jerusalem) - * is left out rather than given one. Names are made plain text. + * code. Ids are kept in data/reference/heritage-ids.json by UNESCO's own site number, so a + * site UNESCO renames keeps its id and visits saved against it still match. A site the + * source has no coordinates for keeps lat 0 / lon 0, read as "no coordinates", never + * guessed. The one site without a country code (Old City of Jerusalem) is left out rather + * than given one. Names are made plain text. * - * Moved from Postcards' scripts/build-heritage-full.mjs; the output is the same. + * Moved from Postcards' scripts/build-heritage-full.mjs. */ import { readFileSync } from "node:fs"; import { gunzipSync } from "node:zlib"; import { join } from "node:path"; import { REFERENCE_DIR, type Built } from "./common"; -interface SourceSite { +export interface SourceSite { id_no: number | string; name_en: string; category: string; @@ -68,57 +71,67 @@ function slugify(name: string): string { const CATEGORY: Record = { Cultural: "cultural", Natural: "natural", Mixed: "mixed" }; +/** A site's entries: one per country, keyed "-". */ +function entries(s: SourceSite): { key: string; candidate: string; site: Omit }[] { + const name = plainName(s.name_en); + const category = CATEGORY[s.category]; + if (!name || !category) throw new Error(`heritage: site ${s.id_no} has a bad name or category`); + const isos = [ + ...new Set( + String(s.iso_codes ?? "") + .split(",") + .map((c) => c.trim().toUpperCase()) + .filter((c) => /^[A-Z]{2}$/.test(c)), + ), + ]; + const c = s.coordinates; + const has = typeof c?.lat === "number" && typeof c?.lon === "number" && Number.isFinite(c.lat) && Number.isFinite(c.lon); + const lat = has ? Number(c!.lat!.toFixed(4)) : 0; + const lon = has ? Number(c!.lon!.toFixed(4)) : 0; + const slug = slugify(name); + return isos.map((iso) => ({ + key: `${s.id_no}-${iso}`, + candidate: isos.length > 1 ? `whs-${slug}-${iso.toLowerCase()}` : `whs-${slug}`, + site: { name, countryIso2: iso, lat, lon, category }, + })); +} + +/** + * Every entry's id: the one it already has, or a new one from its name. A new id that + * another site has or had, or that two new entries would share, takes the site's number. + * Ids are never reused: the result keeps those of sites no longer listed. + */ +export function heritageIds(sites: SourceSite[], known: Record): Record { + const taken = new Set(Object.values(known)); + const fresh = sites.flatMap((s) => entries(s).map((e) => ({ ...e, idNo: String(s.id_no) }))).filter((e) => !known[e.key]); + const counts = new Map(); + for (const e of fresh) counts.set(e.candidate, (counts.get(e.candidate) ?? 0) + 1); + const ids = { ...known }; + for (const e of fresh) { + ids[e.key] = taken.has(e.candidate) || (counts.get(e.candidate) ?? 0) > 1 ? `${e.candidate}-${e.idNo}` : e.candidate; + } + return Object.fromEntries(Object.entries(ids).sort(([a], [b]) => a.localeCompare(b, "en", { numeric: true }))); +} + export function buildHeritage(): Built { - const sites = JSON.parse(gunzipSync(readFileSync(join(REFERENCE_DIR, "whc-sites-2025.json.gz"))).toString("utf8")) as SourceSite[]; + const sites = JSON.parse(gunzipSync(readFileSync(join(REFERENCE_DIR, "whc-sites.json.gz"))).toString("utf8")) as SourceSite[]; if (!Array.isArray(sites) || sites.length < 1100) throw new Error("heritage: unexpected source"); + const ids = heritageIds(sites, JSON.parse(readFileSync(join(REFERENCE_DIR, "heritage-ids.json"), "utf8")) as Record); - const out: (HeritageSite & { idNo?: string })[] = []; + const out: HeritageSite[] = []; const sitesKept = new Set(); for (const s of sites) { - const name = plainName(s.name_en); - const category = CATEGORY[s.category]; - if (!name || !category) throw new Error(`heritage: site ${s.id_no} has a bad name or category`); - const isos = [ - ...new Set( - String(s.iso_codes ?? "") - .split(",") - .map((c) => c.trim().toUpperCase()) - .filter((c) => /^[A-Z]{2}$/.test(c)), - ), - ]; - if (isos.length === 0) continue; - sitesKept.add(String(s.id_no)); - const c = s.coordinates; - const has = typeof c?.lat === "number" && typeof c?.lon === "number" && Number.isFinite(c.lat) && Number.isFinite(c.lon); - const lat = has ? Number(c!.lat!.toFixed(4)) : 0; - const lon = has ? Number(c!.lon!.toFixed(4)) : 0; - const slug = slugify(name); - for (const iso of isos) { - out.push({ - id: isos.length > 1 ? `whs-${slug}-${iso.toLowerCase()}` : `whs-${slug}`, - name, - countryIso2: iso, - lat, - lon, - category, - idNo: String(s.id_no), - }); + for (const e of entries(s)) { + sitesKept.add(String(s.id_no)); + out.push({ id: ids[e.key]!, ...e.site }); } } - - // Two sites can share a slug: the source's own id tells them apart. - const counts = new Map(); - for (const e of out) counts.set(e.id, (counts.get(e.id) ?? 0) + 1); - for (const e of out) { - if ((counts.get(e.id) ?? 0) > 1) e.id = `${e.id}-${e.idNo}`; - delete e.idNo; - } out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); - const ids = new Set(); + const seen = new Set(); for (const e of out) { - if (ids.has(e.id)) throw new Error(`heritage: duplicate id ${e.id}`); - ids.add(e.id); + if (seen.has(e.id)) throw new Error(`heritage: duplicate id ${e.id}`); + seen.add(e.id); if (!/^whs-[a-z0-9-]+$/.test(e.id) || /[<>]/.test(e.name) || Math.abs(e.lat) > 90 || Math.abs(e.lon) > 180) { throw new Error(`heritage: bad entry ${e.id}`); } diff --git a/src/reference/refresh.ts b/src/reference/refresh.ts new file mode 100644 index 0000000..cfe42ce --- /dev/null +++ b/src/reference/refresh.ts @@ -0,0 +1,107 @@ +/** + * The sources `npm run refresh` keeps current, and how each download becomes the saved copy + * in data/reference/ that the builders read. Only the fields the builders use are kept. + * Every function here throws on anything unexpected, so a bad download keeps the old copy. + */ +import type { SourceCountry } from "./countries"; +import { heritageIds, type SourceSite } from "./heritage"; + +const UA = "offware-data-refresh (+https://github.com/offware-apps/data)"; + +export const GEONAMES = "https://download.geonames.org/export/dump/"; +export const COUNTRIES_URL = "https://raw.githubusercontent.com/mledoze/countries/master/countries.json"; +export const UNESCO_META_URL = "https://data.unesco.org/api/explore/v2.1/catalog/datasets/whc001"; +export const UNESCO_EXPORT_URL = `${UNESCO_META_URL}/exports/json?select=id_no,name_en,iso_codes,category,coordinates`; + +/** What was last seen of one remote file. */ +export interface Seen { + url: string; + etag?: string; + lastModified?: string; + /** SHA-256 of the body. */ + sha256?: string; + /** UNESCO's own "modified" date for its dataset. */ + modified?: string; +} + +/** A refresh may lose at most 5% of a source's records; anything worse is a bad download. */ +export function enough(next: number, saved: number): boolean { + return next >= saved * 0.95; +} + +/** + * One conditional GET: "not modified" when the server says so (ETag or date) or when the body + * hashes the same as last time. Never retried: a failure waits for next week. + */ +export async function get(url: string, seen: Seen | undefined, timeoutMs = 120_000): Promise<{ body: Buffer; seen: Seen } | null> { + const { createHash } = await import("node:crypto"); + const headers: Record = { "user-agent": UA }; + if (seen?.url === url && seen.etag) headers["if-none-match"] = seen.etag; + if (seen?.url === url && seen.lastModified) headers["if-modified-since"] = seen.lastModified; + const res = await fetch(url, { headers, signal: AbortSignal.timeout(timeoutMs) }); + if (res.status === 304) return null; + if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`); + const body = Buffer.from(await res.arrayBuffer()); + const sha256 = createHash("sha256").update(body).digest("hex"); + if (seen?.url === url && seen.sha256 === sha256) return null; + const next: Seen = { url, sha256 }; + const etag = res.headers.get("etag"); + const lastModified = res.headers.get("last-modified"); + if (etag) next.etag = etag; + if (lastModified) next.lastModified = lastModified; + return { body, seen: next }; +} + +/** + * cities.tsv from GeoNames' cities1000.txt. Cities already saved keep their place in the + * file; new ones follow by id. A saved city GeoNames no longer lists is kept as it was, so + * its id still resolves. + */ +export function citiesTsv(cities1000: string, saved: string[][]): string[][] { + const fresh = new Map(); + for (const line of cities1000.split("\n")) { + const f = line.split("\t"); + if (f.length < 15 || !/^\d+$/.test(f[0]!) || !/^[A-Z]{2}$/.test(f[8]!)) continue; + const lat = Number(f[4]); + const lon = Number(f[5]); + if (!f[1] || !f[4] || !f[5] || !Number.isFinite(lat) || !Number.isFinite(lon) || /[\t\r]/.test(f[1])) continue; + fresh.set(f[0]!, [f[0]!, f[1], f[8]!, f[10] ?? "", "", Number(f[14]) > 0 ? String(Number(f[14])) : "", f[4], f[5]]); + } + if (!enough(fresh.size, saved.length)) throw new Error(`cities: ${fresh.size} cities, ${saved.length} saved`); + const rows = saved.map((row) => fresh.get(row[0]!) ?? row); + const savedIds = new Set(saved.map((row) => row[0])); + const added = [...fresh.values()].filter((row) => !savedIds.has(row[0])).sort((a, b) => Number(a[0]) - Number(b[0])); + return [...rows, ...added]; +} + +/** admin1CodesASCII.txt: "CC.code\tname\tasciiname\tgeonameid" lines. */ +export function admin1Codes(text: string, savedLines: number): string { + const lines = text.split("\n").filter(Boolean); + if (!lines.every((l) => /^[A-Z]{2}\.[^\t]+\t[^\t]+\t/.test(l)) || !enough(lines.length, savedLines)) { + throw new Error(`admin-1 codes: unexpected file (${lines.length} lines)`); + } + return lines.join("\n") + "\n"; +} + +/** countries.json from mledoze/countries. */ +export function countriesJson(text: string, saved: number): SourceCountry[] { + const world = JSON.parse(text) as SourceCountry[]; + if (!Array.isArray(world) || !enough(world.length, saved)) throw new Error("countries: unexpected file"); + return world.map((c) => { + if (!/^[A-Z]{2}$/.test(c.cca2) || typeof c.name?.common !== "string" || typeof c.region !== "string" || typeof c.unMember !== "boolean") { + throw new Error(`countries: bad entry ${JSON.stringify(c).slice(0, 80)}`); + } + return { cca2: c.cca2, region: c.region, unMember: c.unMember, name: { common: c.name.common }, ...(c.languages ? { languages: c.languages } : {}) }; + }); +} + +/** The UNESCO list, and every site's id: those it had, and new ones for new sites. */ +export function unescoSites(text: string, saved: number, ids: Record): { sites: SourceSite[]; ids: Record } { + const list = JSON.parse(text) as SourceSite[]; + if (!Array.isArray(list) || !enough(list.length, saved)) throw new Error("heritage: unexpected file"); + const sites = list + .map((s) => ({ id_no: String(s.id_no), name_en: s.name_en, iso_codes: s.iso_codes, category: s.category, coordinates: s.coordinates })) + .sort((a, b) => Number(a.id_no) - Number(b.id_no)); + if (sites.some((s) => !/^\d+$/.test(s.id_no))) throw new Error("heritage: a site without a number"); + return { sites, ids: heritageIds(sites, ids) }; +} diff --git a/src/sources.ts b/src/sources.ts index 72844c4..7cd4863 100644 --- a/src/sources.ts +++ b/src/sources.ts @@ -51,7 +51,7 @@ export const SOURCES: Source[] = [ url: "https://www.geonames.org", license: "CC-BY-4.0", licenseUrl: "https://creativecommons.org/licenses/by/4.0/", - use: "Every city (by way of the all-the-cities package), each city's first-level region code and each code's name (export of 2026-09-30).", + use: "Every city of 1,000 people or more, each city's first-level region code and each code's name. Checked for changes monthly.", attribution: "Contains data from GeoNames (https://www.geonames.org), CC BY 4.0.", fix: "Errors are corrected on geonames.org.", }, @@ -79,7 +79,7 @@ export const SOURCES: Source[] = [ url: "https://whc.unesco.org/en/list/", license: "CC-BY-SA-3.0-IGO", licenseUrl: "https://creativecommons.org/licenses/by-sa/3.0/igo/", - use: "Every World Heritage site through the 47th session (2025): name, countries, coordinates and category. Some moments are drawn from it.", + use: "Every World Heritage site: name, countries, coordinates and category. Checked for changes weekly. Some moments are drawn from it.", attribution: "© UNESCO World Heritage Centre (https://whc.unesco.org), CC BY-SA 3.0 IGO.", }, { @@ -103,11 +103,11 @@ export const SOURCES: Source[] = [ }, { id: "world-countries", - name: "world-countries (mledoze/countries)", + name: "mledoze/countries", url: "https://github.com/mledoze/countries", license: "ODbL-1.0", licenseUrl: ODBL, - use: "Each country's common name, region, languages and UN membership.", + use: "Each country's common name, region, languages and UN membership. Checked for changes weekly.", attribution: "Contains information from mledoze/countries (https://github.com/mledoze/countries), made available under the ODbL.", }, { diff --git a/tests/reference.test.ts b/tests/reference.test.ts index 14bbdba..ab2f53d 100644 --- a/tests/reference.test.ts +++ b/tests/reference.test.ts @@ -59,14 +59,15 @@ describe("reference datasets", () => { ); it("gives every city GeoNames lists the region code it lists today", () => { - const current = vendored("city-admin1.tsv.gz"); - const wrong = cities - .filter((c) => { - const code = current.get(c.id); - const listed = code?.startsWith(`${c.countryIso2}.`) && !code.endsWith("."); - return listed && c.subdivisionId !== code!.replace(".", "-"); - }) - .map((c) => `${c.name} ${c.id}: ${c.subdivisionId}`); + const saved = new Map( + gunzipSync(readFileSync(join(REFERENCE_DIR, "cities.tsv.gz"))) + .toString("utf8") + .split("\n") + .filter(Boolean) + .map((line) => line.split("\t")) + .map(([id = "", , country = "", admin1 = ""]) => [id, admin1 ? `${country}-${admin1}` : null]), + ); + const wrong = cities.filter((c) => saved.get(c.id) && c.subdivisionId !== saved.get(c.id)).map((c) => `${c.name} ${c.id}: ${c.subdivisionId}`); expect(wrong).toEqual([]); }); diff --git a/tests/refresh.test.ts b/tests/refresh.test.ts new file mode 100644 index 0000000..1e0dc3b --- /dev/null +++ b/tests/refresh.test.ts @@ -0,0 +1,63 @@ +import { readFileSync } from "node:fs"; +import { gunzipSync } from "node:zlib"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { REFERENCE_DIR } from "../src/reference/common"; +import type { SourceSite } from "../src/reference/heritage"; +import { admin1Codes, citiesTsv, countriesJson, enough, unescoSites } from "../src/reference/refresh"; + +const savedCities = gunzipSync(readFileSync(join(REFERENCE_DIR, "cities.tsv.gz"))) + .toString("utf8") + .split("\n") + .filter(Boolean) + .map((line) => line.split("\t")); + +/** A cities1000.txt line, as GeoNames writes it. */ +const geonames = (id: string, name: string, cc: string, admin1: string, population: string, lat = "1.5", lon = "2.5") => + [id, name, name, "", lat, lon, "P", "PPL", cc, "", admin1, "", "", "", population, "", "10", "Europe/Paris", "2026-10-01"].join("\t"); + +describe("source refresh", () => { + it("leaves the saved cities as they are when GeoNames lists the same", () => { + const text = savedCities + .filter((r) => r[4] !== "u") + .map(([id = "", name = "", cc = "", admin1 = "", , population = "", lat = "", lon = ""]) => geonames(id, name, cc, admin1, population || "0", lat, lon)) + .join("\n"); + expect(citiesTsv(text, savedCities)).toEqual(savedCities); + }); + + it("updates a city in place, adds new ones after, and keeps one GeoNames dropped", () => { + const saved = [ + ["2", "Old", "FR", "11", "", "", "1", "1"], + ["1", "Gone", "FR", "", "u", "", "1", "1"], + ]; + const rows = citiesTsv([geonames("2", "Renamed", "FR", "44", "1200"), geonames("5", "New", "DE", "02", "0"), geonames("3", "Newer", "DE", "02", "5000")].join("\n"), saved); + expect(rows).toEqual([ + ["2", "Renamed", "FR", "44", "", "1200", "1.5", "2.5"], + ["1", "Gone", "FR", "", "u", "", "1", "1"], + ["3", "Newer", "DE", "02", "", "5000", "1.5", "2.5"], + ["5", "New", "DE", "02", "", "", "1.5", "2.5"], + ]); + }); + + it("keeps the saved copy when a download loses more than 5%", () => { + expect(enough(95, 100)).toBe(true); + expect(enough(94, 100)).toBe(false); + expect(() => citiesTsv(geonames("1", "Only", "FR", "11", "1"), savedCities)).toThrow(); + expect(() => admin1Codes("maintenance", 10)).toThrow(); + expect(() => countriesJson("[]", 250)).toThrow(); + expect(() => countriesJson('[{"cca2":"fr"}]', 1)).toThrow(); + }); + + it("keeps the fields the countries build reads", () => { + const world = [{ cca2: "FR", region: "Europe", unMember: true, name: { common: "France", official: "French Republic" }, languages: { fra: "French" }, capital: ["Paris"] }]; + expect(countriesJson(JSON.stringify(world), 1)).toEqual([{ cca2: "FR", region: "Europe", unMember: true, name: { common: "France" }, languages: { fra: "French" } }]); + }); + + it("keeps a UNESCO site's id when it is renamed, and never gives a new site an old id", () => { + const site = (id_no: string, name_en: string, iso_codes = "fr"): SourceSite => ({ id_no, name_en, iso_codes, category: "Cultural", coordinates: { lat: 1, lon: 2 } }); + const known = { "1-FR": "whs-old-town", "9-FR": "whs-harbour" }; + const { sites, ids } = unescoSites(JSON.stringify([site("7", "Harbour"), site("1", "Old Town and its Walls")]), 2, known); + expect(sites.map((s) => s.id_no)).toEqual(["1", "7"]); + expect(ids).toEqual({ "1-FR": "whs-old-town", "7-FR": "whs-harbour-7", "9-FR": "whs-harbour" }); + }); +});