Skip to content

How to geocode addresses

The weather module provides two ways to resolve coordinates without hard-coding them:

  • geocode(address) — convert a known street address to (lat, lon) via the free Nominatim (OpenStreetMap) service.
  • detect_location() — auto-detect the current machine's approximate coordinates from its public IP.

Both return a (latitude, longitude) tuple and raise GeocodingError on failure, so they compose interchangeably with StationIndex.nearest().

How weather is installed

Weather comes with the library. pip install idfkit installs the station index whether or not you asked for it, because a Python extra gates dependencies rather than files.

pip install idfkit

Weather is an opt-in install and the shared name does not reach it. npm install idfkit places no station index on disk and no weather code in your bundle; add the package by name.

npm install @idfkit/weather

Basic Usage

from idfkit.weather import geocode

# Get coordinates for an address
lat, lon = geocode("350 Fifth Avenue, New York, NY")
print(f"Empire State Building: {lat:.4f}, {lon:.4f}")

Combine with StationIndex.nearest() for address-based weather station lookup:

from idfkit.weather import StationIndex, geocode

index = StationIndex.load()

# One-liner using splat operator
results = index.nearest(*geocode("Willis Tower, Chicago, IL"))

# First result is the nearest station
station = results[0].station
print(f"Nearest: {station.display_name} ({results[0].distance_km:.1f} km)")

Address Formats

The geocoder accepts various address formats:

# Full address
lat, lon = geocode("123 Main Street, Springfield, IL 62701")

# Landmark
lat, lon = geocode("Eiffel Tower, Paris")

# City only
lat, lon = geocode("Tokyo, Japan")

# Partial address
lat, lon = geocode("Times Square, New York")

Error Handling

from idfkit.weather import geocode, GeocodingError

try:
    lat, lon = geocode("Nonexistent Place XYZ123")
except GeocodingError as e:
    print(f"Geocoding failed: {e}")

Common Errors

Situation Behavior
Address not found Raises GeocodingError
Network error Raises GeocodingError
Rate limiting Avoided — calls are serialized to ≤1 request/second

Rate Limiting

The free Nominatim service allows at most one request per second. geocode() serializes calls to honour that limit for you, so you don't have to throttle calls yourself:

# These are automatically spaced 1 second apart
for address in addresses:
    lat, lon = geocode(address)  # Rate limited internally
    print(f"{address}: {lat:.2f}, {lon:.2f}")

For bulk geocoding, and for why the limit exists, see About geocoding and IP-based location.

Caching Results

For repeated lookups, cache the coordinates:

from functools import lru_cache


@lru_cache(maxsize=100)
def cached_geocode(address: str) -> tuple[float, float]:
    return geocode(address)


# Subsequent calls are instant
lat, lon = cached_geocode("123 Main St")
lat, lon = cached_geocode("123 Main St")  # From cache

Complete Workflow

from idfkit import load_idf
from idfkit.weather import (
    StationIndex,
    WeatherDownloader,
    DesignDayManager,
    geocode,
)
from idfkit.simulation import simulate

# Project location
project_address = "1600 Pennsylvania Avenue, Washington, DC"

# Find nearest weather station
index = StationIndex.load()
lat, lon = geocode(project_address)
results = index.nearest(lat, lon)

station = results[0].station
print(f"Project location: {lat:.4f}, {lon:.4f}")
print(f"Nearest station: {station.display_name} ({results[0].distance_km:.1f} km)")

# Download weather data
downloader = WeatherDownloader()
files = downloader.download(station)

# Load model and apply design days
model = load_idf("building.idf")
ddm = DesignDayManager(files.ddy)
ddm.apply_to_model(model, heating="99.6%", cooling="1%", update_location=True)

# Run simulation
result = simulate(model, files.epw, design_day=True)

Alternative: Direct Coordinates

If you already know the coordinates, skip geocoding entirely:

# Direct coordinate lookup
results = index.nearest(40.7484, -73.9857)  # Empire State Building

Detect Location from IP

detect_location() resolves the running machine's approximate coordinates from its public IP, so callers don't need to know or supply an address. It's the "weather stations near me" companion to geocode().

from idfkit.weather import detect_location

# Detect approximate coordinates from this machine's public IP.
# Sent over HTTPS to ipapi.co; result cached on disk for 1 hour.
lat, lon = detect_location()
print(f"Approximate location: {lat:.4f}, {lon:.4f}")

Like geocode(), the result splats directly into StationIndex.nearest():

from idfkit.weather import StationIndex, detect_location

index = StationIndex.load()

# "Find weather stations near me" — one liner using the splat operator.
results = index.nearest(*detect_location())

station = results[0].station
print(f"Nearest: {station.display_name} ({results[0].distance_km:.1f} km)")

The same flow is exposed on the CLI as idfkit tmy --nearby.

Caching

Calls hit ipapi.co over HTTPS and the result is cached on disk for 1 hour by default. Repeated calls within that window return the cached value with no network access. The cache file lives under the same platform cache directory used by WeatherDownloader (e.g. ~/.cache/idfkit/weather/ipgeo.json on Linux), and the TTL and location are both configurable:

from idfkit.weather import detect_location

# Default: cache for 1 hour in the platform weather cache directory.
lat, lon = detect_location()

# Cache for 24 hours instead.
lat, lon = detect_location(max_age=timedelta(hours=24))

# Always re-fetch (skip the cache).
lat, lon = detect_location(max_age=0)

# Cache forever (until the file is deleted).
lat, lon = detect_location(max_age=None)

# Use a custom cache directory.
lat, lon = detect_location(cache_dir=Path("/tmp/my-cache"))

Error Handling

detect_location() raises GeocodingError on any failure — network outage, ipapi.co rate limit, or an unlocatable IP (e.g. some VPNs):

from idfkit.weather import GeocodingError, detect_location

try:
    lat, lon = detect_location()
except GeocodingError as e:
    # Network failure, ipapi.co rate limit, or unrecognised IP.
    print(f"Could not detect location: {e}")
    # Fall back to an explicit prompt or a hard-coded default.
    lat, lon = 41.85, -87.65  # Chicago

Accuracy is city-level and the call discloses your public IP; see About geocoding and IP-based location for what that means and how to avoid it.

See Also