Metadata-Version: 2.4
Name: django-whosonfirst
Version: 0.1.0
Summary: Django app for importing and managing Who's On First geographic data
Author-email: Arthur Hanson <worldnomad@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/arthanson/django-whosonfirst
Project-URL: Documentation, https://github.com/arthanson/django-whosonfirst#readme
Project-URL: Repository, https://github.com/arthanson/django-whosonfirst
Project-URL: Issues, https://github.com/arthanson/django-whosonfirst/issues
Keywords: django,gis,geography,whosonfirst,gazetteer,postgis
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Scientific/Engineering :: GIS
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=4.2
Requires-Dist: requests>=2.28.0
Requires-Dist: tqdm>=4.64.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-django>=4.5.0; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Dynamic: license-file

# django-whosonfirst

A Django app for importing and managing [Who's On First](https://whosonfirst.org/) geographic data.

Who's On First (WOF) is an open, global gazetteer that provides stable place IDs, explicit hierarchies, and polygon geometries for geographic entities—from continents and countries down to cities and neighbourhoods.

## Features

- Download WOF SQLite data bundles (global or per-country)
- Import places with full hierarchy (continent → country → region → locality → neighbourhood)
- Store geometries in PostGIS
- Map to GeoNames and Wikidata via concordances
- Extensible plugin system for custom processing

## Requirements

- Python 3.10+
- Django 4.2+
- PostgreSQL with PostGIS extension
- `django.contrib.gis` configured

## Installation

```bash
pip install django-whosonfirst
```

Add to your `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    # ...
    'django.contrib.gis',
    'whosonfirst',
]
```

Run migrations:

```bash
python manage.py migrate whosonfirst
```

## Quick Start

### 1. Download WOF Data

Download data for specific countries:

```bash
# Download US and Canada
python manage.py wof_download --countries=US,CA

# Download all countries (large!)
python manage.py wof_download
```

### 2. Import Data

```bash
# Import downloaded data
python manage.py wof_import

# Import specific countries
python manage.py wof_import --countries=US,CA

# Import specific placetypes
python manage.py wof_import --placetypes=country,region,locality

# Import without geometry (faster, smaller database)
python manage.py wof_import --no-geometry
```

### 3. Query Places

```python
from whosonfirst.models import Place

# Get all countries
countries = Place.objects.filter(placetype='country')

# Get cities in the US
us_cities = Place.objects.filter(
    placetype='locality',
    country_code='US'
)

# Get a place by WOF ID
sf = Place.objects.get(wof_id=85922583)  # San Francisco

# Get ancestors
sf.get_ancestors()  # Returns California, US, North America

# Get descendants
ca = Place.objects.get(wof_id=85688637)  # California
ca.get_descendants(placetype='locality')  # All cities in CA

# Query by GeoNames ID
place = Place.objects.get(geonames_id=5391959)

# Query by Wikidata ID
place = Place.objects.get(wikidata_id='Q62')
```

## Management Commands

### wof_download

Download WOF SQLite data bundles.

```bash
# Download specific countries
python manage.py wof_download --countries=US,CA,MX

# Force re-download
python manage.py wof_download --countries=US --force

# List cached bundles
python manage.py wof_download --list

# Clear cache
python manage.py wof_download --clear
```

### wof_import

Import WOF data into Django models.

```bash
# Import all downloaded data
python manage.py wof_import

# Import specific countries
python manage.py wof_import --countries=US

# Import specific placetypes
python manage.py wof_import --placetypes=country,region,locality,neighbourhood

# Flush existing data before import
python manage.py wof_import --flush=all

# Dry run (parse without saving)
python manage.py wof_import --dry-run

# Skip geometry import
python manage.py wof_import --no-geometry

# Skip alternative names
python manage.py wof_import --no-names
```

### wof_update

Update data (download fresh and import changes).

```bash
python manage.py wof_update --countries=US

# Check for changes without importing
python manage.py wof_update --check-only
```

### wof_build_index

Build derived indices and resolve relationships.

```bash
# Run all index operations
python manage.py wof_build_index --all

# Resolve parent relationships only
python manage.py wof_build_index --resolve-parents
```

## Configuration

Configure via Django settings:

```python
# Data directory for downloads (default: app's data/ directory)
WOF_DATA_DIR = '/path/to/wof/data'

# Placetypes to import by default
WOF_PLACETYPES = [
    'continent',
    'country',
    'region',
    'locality',
    'neighbourhood',
]

# Country codes to import (empty = all)
WOF_COUNTRY_CODES = ['US', 'CA', 'MX']

# Whether to import geometry (default: True)
WOF_IMPORT_GEOMETRY = True

# Skip deprecated places (default: True)
WOF_SKIP_DEPRECATED = True

# Skip places marked as not current (default: False)
WOF_SKIP_NOT_CURRENT = False

# Languages to import for alternative names (ISO 639-3 codes)
# Default: ['eng'] (English only)
# Use empty list [] to import all languages
WOF_LANGUAGES = ['eng']

# HTTP download timeout in seconds (default: 300)
WOF_DOWNLOAD_TIMEOUT = 300

# Maximum download size in bytes (default: 10GB)
WOF_MAX_DOWNLOAD_SIZE = 10 * 1024 * 1024 * 1024

# Batch size for bulk operations (default: 1000)
WOF_BATCH_SIZE = 1000

# Plugin classes for custom processing
WOF_PLUGINS = ['myapp.plugins.MyWOFPlugin']
```

## Placetypes

WOF uses a hierarchy of placetypes:

| Placetype | Description | Example |
|-----------|-------------|---------|
| continent | Continents | North America |
| country | Countries | United States |
| dependency | Dependencies | Puerto Rico |
| region | First-level admin (states/provinces) | California |
| county | Counties | Los Angeles County |
| localadmin | Local administrative areas | City of Los Angeles |
| locality | Cities/towns | San Francisco |
| borough | Boroughs | Manhattan |
| neighbourhood | Neighbourhoods | Mission District |
| venue | Points of interest | Golden Gate Bridge |

Default import includes: `continent`, `country`, `dependency`, `region`, `county`, `localadmin`, `locality`

## Models

### Place

The main model storing all WOF places.

```python
class Place(models.Model):
    # Identity
    wof_id = BigIntegerField(primary_key=True)
    placetype = CharField(max_length=50)
    name = CharField(max_length=255)
    slug = SlugField(max_length=255)
    country_code = CharField(max_length=2)

    # Hierarchy
    parent = ForeignKey('self', null=True)
    parent_wof_id = BigIntegerField(null=True)
    belongsto = JSONField(default=list)  # Ancestor WOF IDs
    hierarchy = JSONField(default=list)

    # Geometry (PostGIS)
    geom = MultiPolygonField(null=True, srid=4326)
    centroid = PointField(null=True, srid=4326)
    latitude = FloatField(null=True)
    longitude = FloatField(null=True)
    bbox = JSONField(null=True)

    # Concordances
    concordances = JSONField(default=dict)
    geonames_id = BigIntegerField(null=True)
    wikidata_id = CharField(max_length=20)

    # Metadata
    lastmodified = BigIntegerField(null=True)
    is_current = BooleanField(default=True)
    is_deprecated = BooleanField(default=False)
```

### PlaceName

Alternative names in different languages.

```python
class PlaceName(models.Model):
    place = ForeignKey(Place)
    name = CharField(max_length=255)
    language = CharField(max_length=10)  # ISO 639-3
    name_type = CharField(max_length=20)  # preferred, variant, etc.
    is_preferred = BooleanField(default=False)
```

## Plugins

Create custom plugins to hook into the import process:

```python
# myapp/plugins.py
from whosonfirst.exceptions import HookException

class MyWOFPlugin:
    def place_pre(self, command, item):
        """Called before parsing each place.

        Args:
            command: Management command instance
            item: Raw data dict from parser

        Raises:
            HookException: Skip this item
        """
        # Skip places without geometry
        if not item.get('geometry'):
            raise HookException('No geometry')

        # Modify item in place
        item['name'] = item['name'].title()

    def place_post(self, command, obj, item):
        """Called after saving each place.

        Args:
            command: Management command instance
            obj: Saved Place model instance
            item: Raw data dict
        """
        if obj.placetype == 'locality':
            # Custom processing for cities
            pass
```

Register in settings:

```python
WOF_PLUGINS = ['myapp.plugins.MyWOFPlugin']
```

## Concordances

WOF includes mappings to external datasets. Access via the `concordances` JSONField or dedicated fields:

```python
place = Place.objects.get(name='San Francisco', placetype='locality')

# GeoNames ID
place.geonames_id  # 5391959

# Wikidata QID
place.wikidata_id  # 'Q62'

# All concordances
place.concordances
# {
#     'geonames_id': 5391959,
#     'wikidata_id': 'Q62',
#     'fips_code': '06075',
#     'iso_code': 'US-CA',
#     ...
# }
```

## Disclaimer

This project is not affiliated with, endorsed by, or associated with [Who's On First](https://whosonfirst.org/), [Mapzen](https://www.mapzen.com/), or [Geocode Earth](https://geocode.earth/). It is an independent, third-party Django integration that consumes publicly available Who's On First data.

## License

MIT License

## WOF Data License

Who's On First data is licensed under various open licenses. See [WOF Licenses](https://whosonfirst.org/docs/licenses/) for details.

When using WOF data, please provide appropriate attribution.

## Links

- [Who's On First](https://whosonfirst.org/)
- [WOF Data Downloads](https://whosonfirst.org/download/)
- [WOF GitHub](https://github.com/whosonfirst-data/)
- [WOF Documentation](https://whosonfirst.org/docs/)
