Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<!-- insertion marker -->
## [Unreleased]

### Added

- Add `get_admin1_codes()` method returning first-level administrative division data from the GeoNames `admin1CodesASCII.txt` dataset, keyed by `<countrycode>.<admin1code>` (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

<small>[Compare with 3.0.1](https://github.com/yaph/geonamescache/compare/3.0.1...3.0.2)</small>
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dl:

json:
mkdir -p geonamescache/data/
./bin/admin1.py
./bin/continents.py
./bin/countries.py
./bin/cities.py
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).

Expand Down Expand Up @@ -31,13 +31,16 @@ 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()
* get\_us\_states\_by\_names()
* get\_cities\_by\_name(name)
* get\_us\_counties()

The dictionary returned by `get_admin1_codes()` is keyed by the code `<countrycode>.<admin1code>`, 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)
Expand Down
24 changes: 24 additions & 0 deletions bin/admin1.py
Original file line number Diff line number Diff line change
@@ -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))
1 change: 1 addition & 0 deletions bin/download_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 7 additions & 0 deletions geonamescache/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from typing import Any, ClassVar, TypeVar

from geonamescache.types import (
Admin1,
Admin1CodeStr,
City,
CitySearchAttribute,
Continent,
Expand All @@ -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
Expand All @@ -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 <countrycode>.<admin1code>, 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')

Expand Down
7 changes: 7 additions & 0 deletions geonamescache/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

GeoNameIdStr = str
ISOStr = str
Admin1CodeStr = str
ContinentCode = Literal["AF", "AN", "AS", "EU", "NA", "OC", "SA"]
USStateCode = Literal[
"AK",
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions tests/test_geonamescache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down