How to search for weather stations¶
The StationIndex provides fast searching and filtering of ~70,000 weather
station dataset entries (covering ~17,300 unique physical stations) from
climate.onebuilding.org.
Differs in JavaScript
The weather station index exists in both libraries and does not behave the same way in JavaScript. The ledger records it as Python complete, JavaScript partial, and what differs is stated here rather than left to be discovered.
Installation differs, and the difference is deliberate. pip install idfkit installs
weather and its station index unconditionally, because Python extras gate dependencies
rather than files, so a Python reader has weather whether or not they wanted it. npm
install @idfkit/idfkit installs neither: weather is an opt-in peer there, added with
npm install @idfkit/weather. Both libraries ship their own index once installed and
neither retrieves one to get started (FR-043, FR-075, research R11). A JavaScript reader
who follows a weather page without installing that package gets a resolution error, not
a smaller feature, which is why the packaging is recorded here as a stated difference
rather than left as an undescribed detail.
Freshness handling differs. Python fires a throttled nudge from StationIndex.load(): at most once every 24 hours it probes the upstream KML files, warns when the bundled or cached index is behind, records the check under the cache directory, and can be turned off with IDFKIT_NO_WEATHER_UPDATE_CHECK. JavaScript has no such nudge and no timestamp to throttle against. It exposes checkForUpdates and refreshStationIndex for a caller who asks, and does nothing on its own, because the nudge is built on a writable cache directory and a browser-targeted package has none. The consequence for a reader is concrete: a stale index goes unmentioned in JavaScript until they check for themselves.
The installation difference above is the whole weather surface's, not the index's alone.
It is stated here because the index is where a reader meets it first, and
weather-download and
weather-file-cache refer back to it
rather than repeat it.
The full entry, including the vocabulary this capability owns, is on the capability parity page.
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.
Loading the Index¶
from idfkit.weather import StationIndex
# Instant load from bundled data (no network needed)
index = StationIndex.load()
print(f"Stations: {len(index)}")
print(f"Countries: {len(index.countries)}")
Search by Name¶
Fuzzy text search across station names, cities, and WMO numbers:
# Search by name
results = index.search("chicago ohare")
for r in results[:5]:
print(f"{r.station.display_name} (score={r.score:.2f})")
SearchResult Attributes¶
Search Tips¶
# City name
results = index.search("New York")
# Airport code pattern
results = index.search("JFK")
# WMO number
results = index.search("725300")
# Country + city
results = index.search("London UK")
Search by EPW Filename¶
search() automatically detects canonical EPW filenames and resolves them:
# EPW filenames are detected automatically by search()
results = index.search("USA_IL_Chicago.Ohare.Intl.AP.725300_TMYx.2009-2023")
# Extensions are tolerated
results = index.search("GBR_London.Heathrow.AP.037720_TMYx.epw")
# Works the same as any other search query
for r in results[:3]:
print(f"{r.station.display_name} (score={r.score:.2f})")
For exact lookups when you know the precise filename, use get_by_filename():
# Exact lookup by EPW filename (case-insensitive, extension-tolerant)
stations = index.get_by_filename("USA_IL_Chicago.Ohare.Intl.AP.725300_TMYx.2009-2023")
for station in stations:
print(f"{station.display_name}: {station.dataset_variant}")
Search by Coordinates¶
Find stations nearest to a location using great-circle distance:
# Nearest to downtown Chicago
results = index.nearest(41.88, -87.63)
for r in results[:5]:
print(f"{r.station.display_name}: {r.distance_km:.1f} km")
Function Signature¶
def nearest(
self,
latitude: float,
longitude: float,
*,
limit: int = 5,
max_distance_km: float | None = None,
country: str | None = None,
) -> list[SpatialResult]:
SpatialResult Attributes¶
Search by Address¶
Combine geocode() with nearest() for address-based search:
from idfkit.weather import StationIndex, geocode
index = StationIndex.load()
# One-liner using splat operator
results = index.nearest(*geocode("Willis Tower, Chicago, IL"))
# Or step by step
lat, lon = geocode("350 Fifth Avenue, New York, NY")
results = index.nearest(lat, lon)
Climate-zone-aware search
Each WeatherStation carries its ASHRAE HOF climate zone, design
dry-bulb temperatures, HDD18, and CDD10. Filter on the zone with the
index's own key rather than on the label text: see
Filter by Climate Zone below.
Filter by Country¶
# Get all stations in a country
us_stations = index.filter(country="USA")
print(f"US stations: {len(us_stations)}")
# Get all stations in a state/region
california = [s for s in us_stations if s.state == "CA"]
Filter by Coordinates¶
Use nearest() with max_distance_km to find stations within a geographic area:
# Find all stations within 100 km of a point
stations = index.nearest(
41.0,
-88.5,
max_distance_km=100.0,
limit=50,
)
Get by WMO Number¶
# Get specific station by WMO
results = index.get_by_wmo("725300")
for station in results:
print(f"{station.display_name}: {station.source}")
Note: WMO numbers are not unique — multiple entries can share a WMO (different year ranges, data sources).
WeatherStation Attributes¶
Every field of WeatherStation, with its type and its default, is in the API reference. It is generated from the source, so it cannot fall behind the way the table that used to sit here did.
The five climate metrics (ashrae_climate_zone, the two design DBs,
HDD18, and CDD10) are populated for every station in the bundled index.
design_conditions_source_wmo is only set when a station inherits its
design conditions from a neighbouring WMO station; otherwise it is
None.
Filter by Climate Zone¶
Ask the index for a zone by its code.
# Ask for a zone by its code. The code is parsed out of the label rather than
# read off the front of it, which matters: 2,162 of the 69,638 bundled records
# are labelled "7A - ASHRAE Climate Zone could not be determined" or "8A - ...",
# and neither 7A nor 8A is an ASHRAE zone, since zones 7 and 8 carry no suffix.
zone_4a = index.filter(climate_zone="4A")
print(f"Zone 4A stations: {len(zone_4a)}")
# Combine with the other keys, which all narrow together.
seattle_area = index.filter(climate_zone="4C", country="USA", state="WA")
print(f"Zone 4C in Washington: {len(seattle_area)}")
# The stations whose zone upstream could not determine are reachable, and only
# this way: no climate_zone value returns them. Asking for them is a separate
# question because the zone key's domain is already every real code, so a
# reserved string could not be told apart from one.
undetermined = index.filter(climate_zone_determined=False)
print(f"Zone not determined upstream: {len(undetermined)}")
# Pick the warmest design dry-bulb in a zone.
hottest = max(index.filter(climate_zone="5A", country="USA"), key=lambda s: s.cooling_design_db_c)
print(f"{hottest.display_name}: {hottest.cooling_design_db_c} °C / {hottest.cooling_design_db_f:.1f} °F")
// Ask for a zone by its code. The code is parsed out of the label rather than
// read off the front of it, which matters: 2,162 of the 69,638 bundled records
// are labelled '7A - ASHRAE Climate Zone could not be determined' or '8A - ...',
// and neither 7A nor 8A is an ASHRAE zone, since zones 7 and 8 carry no suffix.
const zone4a = index.filter({ climateZone: '4A' });
// Combine with the other keys, which all narrow together.
const seattleArea = index.filter({ climateZone: '4C', country: 'USA', state: 'WA' });
// The stations whose zone upstream could not determine are reachable, and only
// this way: no climateZone value returns them. Asking for them is a separate
// question because the zone key's domain is already every real code, so a
// reserved string could not be told apart from one.
const undetermined = index.filter({ climateZoneDetermined: false });
The label is not a code, and matching its first token invents two zones
2,162 of the 69,638 bundled records are labelled
7A - ASHRAE Climate Zone could not be determined or 8A - .... Neither 7A
nor 8A is an ASHRAE zone: zones 7 and 8 carry no moisture suffix. Reading the
code off the front of the label therefore produces twenty-one zones where
there are nineteen, and files 3.1% of the index under two that do not exist.
The zone key matches a parsed code with those records excluded, so it returns
nothing for 7A. They stay reachable through the separate key for records
whose zone was not determined, and every station is returned by exactly one of
the two.
A related trap, for anyone writing the parse by hand: the suffix is [ABC],
not [AB]. Dropping C loses 3C, 4C and 5C, which is 1,653 marine-zone
stations, leaves sixteen zones where there are nineteen, and raises nothing.
Listing Countries¶
# Get all available countries
countries = index.countries
for country in sorted(countries)[:10]:
count = len(index.filter(country=country))
print(f"{country}: {count} stations")
Refreshing the Index¶
The bundled index works without network access. To get the latest data:
# Check if upstream has updates
if index.check_for_updates():
print("Updates available")
# Refresh from climate.onebuilding.org (stdlib only, no extras)
index = StationIndex.refresh()
Refresh uses the Python standard library only — no third-party packages
required. The same operation is available from the shell as
idfkit tmy --refresh — see idfkit tmy.
Performance¶
The index uses efficient data structures for fast searching:
| Operation | Typical Time |
|---|---|
load() |
~100ms |
search(query) |
~10ms |
nearest(lat, lon) |
~50ms |
filter(country=...) |
~5ms |
Best Practices¶
- Load once — Keep the index in memory for multiple searches
- Use spatial search — More accurate than name matching for locations
- Check multiple results — First result isn't always the best match
- Verify WMO — Same physical station may have multiple entries
See Also¶
- How to download weather files — Download files for a station
- How to geocode addresses — Convert addresses to coordinates
- Weather Pipeline — Architecture details