diff --git a/CHANGELOG.md b/CHANGELOG.md index 68cc655..28aa43a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Add `get_admin1_codes()` method returning first-level administrative division data from the GeoNames `admin1CodesASCII.txt` dataset, keyed by `.` (e. g. `US.CA`), which allows resolving the `countrycode`/`admin1code` references stored in city records. + ## [3.0.2](https://github.com/yaph/geonamescache/releases/tag/3.0.2) - 2026-07-28 [Compare with 3.0.1](https://github.com/yaph/geonamescache/compare/3.0.1...3.0.2) diff --git a/Makefile b/Makefile index 3ff4b16..f8715fb 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ dl: json: mkdir -p geonamescache/data/ + ./bin/admin1.py ./bin/continents.py ./bin/countries.py ./bin/cities.py diff --git a/README.md b/README.md index 9c37924..eee28e0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![image](https://img.shields.io/pypi/v/geonamescache.svg)](https://pypi.python.org/pypi/geonamescache) -A Python library that provides functions to retrieve names, ISO and FIPS codes of continents, countries as well as US states and counties as Python dictionaries. The country and city datasets also include population and geographic data. +A Python library that provides functions to retrieve names, ISO and FIPS codes of continents, countries and first-level administrative divisions as well as US states and counties as Python dictionaries. The country and city datasets also include population and geographic data. Geonames data is obtained from [GeoNames](http://www.geonames.org/). @@ -31,6 +31,7 @@ Currently geonamescache provides the following methods, that return dictionaries * get\_continents() * get\_countries() +* get\_admin1\_codes() * get\_us\_states() * get\_cities() * get\_countries\_by\_names() @@ -38,6 +39,8 @@ Currently geonamescache provides the following methods, that return dictionaries * get\_cities\_by\_name(name) * get\_us\_counties() +The dictionary returned by `get_admin1_codes()` is keyed by the code `.`, for example `US.CA` for California, which allows resolving the `countrycode` and `admin1code` references stored in city records. + In addition you can search for cities by name. * search\_cities(\'NAME\', case\_sensitive=True, contains\_search=True) diff --git a/bin/admin1.py b/bin/admin1.py new file mode 100755 index 0000000..a66f0fb --- /dev/null +++ b/bin/admin1.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +import csv +import json +from pathlib import Path + +p_data = Path('datasets') + +admin1 = {} + +reader = csv.reader(p_data.joinpath('admin1CodesASCII.txt').open(encoding='utf-8'), 'excel-tab') +for record in reader: + code, name, asciiname, geonameid = record + + # required because used as key + if not code: + continue + + admin1[code] = { + 'asciiname': asciiname, + 'geonameid': int(geonameid) if geonameid else 0, + 'name': name, + } + +p_data.joinpath('admin1.json').write_text(json.dumps(admin1, ensure_ascii=False)) diff --git a/bin/download_data.py b/bin/download_data.py index 57c5932..2817992 100755 --- a/bin/download_data.py +++ b/bin/download_data.py @@ -8,6 +8,7 @@ # Data files to download DOWNLOADS = [ + 'http://download.geonames.org/export/dump/admin1CodesASCII.txt', 'http://download.geonames.org/export/dump/cities500.zip', 'http://download.geonames.org/export/dump/cities1000.zip', 'http://download.geonames.org/export/dump/cities5000.zip', diff --git a/geonamescache/__init__.py b/geonamescache/__init__.py index 20f2a43..5ca2f83 100644 --- a/geonamescache/__init__.py +++ b/geonamescache/__init__.py @@ -10,6 +10,8 @@ from typing import Any, ClassVar, TypeVar from geonamescache.types import ( + Admin1, + Admin1CodeStr, City, CitySearchAttribute, Continent, @@ -27,6 +29,7 @@ class GeonamesCache: + admin1: dict[Admin1CodeStr, Admin1] | None = None continents: dict[ContinentCode, Continent] | None = None countries: dict[ISOStr, Country] | None = None cities: dict[GeoNameIdStr, City] | None = None @@ -47,6 +50,10 @@ def get_continents(self) -> dict[ContinentCode, Continent]: def get_countries(self) -> dict[ISOStr, Country]: return self._load_data(self.countries, 'countries.json') + def get_admin1_codes(self) -> dict[Admin1CodeStr, Admin1]: + """Get first-level administrative divisions keyed by ., e. g. US.CA.""" + return self._load_data(self.admin1, 'admin1.json') + def get_us_states(self) -> dict[USStateCode, USState]: return self._load_data(self.us_states, 'us_states.json') diff --git a/geonamescache/types.py b/geonamescache/types.py index 5a700d1..ecf282a 100644 --- a/geonamescache/types.py +++ b/geonamescache/types.py @@ -8,6 +8,7 @@ GeoNameIdStr = str ISOStr = str +Admin1CodeStr = str ContinentCode = Literal["AF", "AN", "AS", "EU", "NA", "OC", "SA"] USStateCode = Literal[ "AK", @@ -167,6 +168,12 @@ class Continent(TypedDict): cc2: NotRequired[str] +class Admin1(TypedDict): + asciiname: str + geonameid: int + name: str + + class City(TypedDict): alternatenames: list[str] admin1code: str diff --git a/tests/test_geonamescache.py b/tests/test_geonamescache.py index 123dbc1..fe68633 100644 --- a/tests/test_geonamescache.py +++ b/tests/test_geonamescache.py @@ -3,6 +3,26 @@ gc = GeonamesCache() +def test_get_admin1_codes(): + admin1 = gc.get_admin1_codes() + assert len(admin1) > 3000 + for key, name, geonameid in ( + ('US.CA', 'California', 5332921), + ('ES.51', 'Andalusia', 2593109), + ): + assert name == admin1[key]['name'] + assert geonameid == admin1[key]['geonameid'] + + +def test_admin1_code_resolves_city_reference(): + # Cities store countrycode and admin1code separately, the composite + # admin1 key allows resolving these references. + city = gc.get_cities()['5368361'] + assert 'Los Angeles' == city['name'] + key = f"{city['countrycode']}.{city['admin1code']}" + assert 'California' == gc.get_admin1_codes()[key]['name'] + + def test_get_countries_by_names(): # Length of get_countries_by_names dict and get_countries dict must be # the same, unless country names wouldn't be unique.