Skip to content

Geocoding

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().

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 limited Automatically retries with delay

Rate Limiting

Nominatim requires a maximum of 1 request per second. The geocode() function automatically handles rate limiting:

# 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}")

No API Key Required

Nominatim is a free service that doesn't require an API key. However:

  • Be respectful of usage limits
  • Avoid bulk geocoding (use batch geocoding services for large datasets)
  • Cache results when possible

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)

Accuracy Notes

  • Geocoding accuracy varies by location and address specificity
  • Results may vary slightly over time as OpenStreetMap data is updated
  • For critical applications, verify coordinates manually

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

Privacy and Accuracy Notes

  • Calling detect_location() sends your machine's public IP address to ipapi.co over HTTPS. If you'd rather not, use geocode("city, country") or pass coordinates directly.
  • Accuracy is city-level. That is sufficient for choosing a TMYx station within ~50 km, but not for precise positioning.
  • The cache is on the local filesystem; nothing is sent anywhere else.

See Also