timezone_finder
π― Add-on for
package:timezone: find theLocationfor a point on Earth; add metazone labels and multi-place helpers to theLocationandTZDateTimeyou already hold.
Where you run it
- VM, CLI, server, Flutter mobile and desktop β import
package:timezone_finder/timezone_finder.dart. Boundaries are embedded; no install step. - Web and Flutter web β import
package:timezone_finder/browser.dartand install the packed.binbefore any lookup (see Web / Flutter web). Copy-pasting the VM sample onto web will throw.
What you get
- OpenStreetMap borders (Timezone Boundary Builder) β IANA zone for a longitude/latitude or GeoJSON Point, offline β no network
- IANA tzdb (via
package:timezone) β offsets, DST, and abbreviations on realLocation/TZDateTimevalues - Unicode CLDR β English metazone names (Pacific Time, Central European Summer Time, β¦).
- Extensions, not new classes β nothing to construct, no wrapper types; facts live on
LocationandTZDateTime(String.toLocation,Location.metazoneName,TZDateTime.convertTo, β¦)
A metazone is CLDR's grouping of zones that share a display name: Paris, Madrid and Zurich are all Central European Time.
longitude, latitude ββ local time
βββ Location ββ TZDateTime ββ metazone name
GeoJSON Point/Feature ββ abbreviation
Coordinates come from wherever you already have them β a GPS fix, a map tap, a database column. If you start from an address, Nominatim and Photon are OpenStreetMap-based geocoders whose answers are GeoJSON, which toLocation reads directly.
Coordinates only (VM / mobile / desktop / CLI) β needs latest_all so the
IANA id can become a Location (Location.name is that identifier):
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone_finder/timezone_finder.dart';
tz.initializeTimeZones();
findLocation(2.3522, 48.8566)?.name; // 'Europe/Paris' β longitude, latitude
findLocation(-140.0, 0.0); // null β no land polygon
With GeoJSON and multi-place helpers:
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone_finder/timezone_finder.dart';
tz.initializeTimeZones(); // required before any Location exists
// GeoJSON from Nominatim (ask for it: &format=geojson) or Photon (default).
// Coordinates are [longitude, latitude].
const cdg = '{"type": "Point", "coordinates": [2.5479, 49.0097]}';
const jfk = '{"type": "Point", "coordinates": [-73.7781, 40.6413]}';
final paris = cdg.toLocation()!; // a Location, straight from GeoJSON
paris.name; // 'Europe/Paris' β store this
paris.metazoneName; // 'Central European Time'
final takeOff = tz.TZDateTime(paris, 2026, 8, 23, 10, 15);
final landing = takeOff.add(const Duration(hours: 8, minutes: 20));
final jfkLoc = jfk.toLocation()!;
// convertTo returns a new value; landing stays in Paris (CEST).
final atJfk = landing.convertTo(jfkLoc); // 12:35 at JFK β same instant
takeOff.utcOffsetDifference(jfkLoc); // -6 hours (signed; JFK behind Paris)
atJfk.metazoneName; // 'Eastern Daylight Time'
atJfk.metazoneAbbreviation; // 'EDT'
atJfk.utcOffset; // 'UTC-04'
API
Top-level β from coordinates, synchronous, offline (after tzdata init):
findLocation(longitude, latitude)β aLocation, ornullwhen no land polygon covers the point.Location.nameis the IANA identifier to store.'{"type":"Point",β¦}'.toLocation()β the same, from a geocoder's GeoJSONensurePreloaded()β decode the index at startup (once per isolate) instead of on first useianaDatabaseVersion,cldrVersion
On Location β a place, so nothing here depends on an instant:
metazoneNameβ Pacific Time (English). Season-neutral by design;nullfor nine zones CLDR leaves unnamed. There is deliberately no abbreviation: every tzdb abbreviation is either standard or daylight, so a place could only ever answer for half the year.
On TZDateTime β a place and a moment:
convertTo(location)β the same instant, on another clockutcOffsetDifference(location)β signed UTC-offset gap at this instant (positive = other place ahead); notTZDateTime.difference(elapsed time). Use.abs()for a directionless magnitudeutcOffsetβUTC-07/UTC+04:30/UTCβ always the numeric offset form, never a letter abbreviation likePDTmetazoneNameβ Pacific Daylight Time β English, standard or daylight, at this instant;nullwhere CLDR has no namemetazoneAbbreviationβPDT, orUTC+04:30when tzdb's value is numeric (always aStringβ tzdb names every instant)
Web / Flutter web
On web the index is not embedded as Dart source. Hand the packed .bin (~4.01 MB) to installBoundaries before any lookup β however your app gets bytes: an asset bundle, your own CDN, a cache. The filename tracks the boundary data release (boundaries_<ianaDatabaseVersion>.bin, currently boundaries_2026c.bin) β so a boundary release changes the path your app declares and loads, and an upgrade that skips it fails at the fetch or the asset lookup, not at compile time.
Flutter web. Declare the asset in your app's pubspec.yaml. The package deliberately does not declare it, so Android and iOS builds β which use the embedded index β do not carry 4 MB they never read:
flutter:
assets:
- packages/timezone_finder/data/boundaries_2026c.bin
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:timezone/browser.dart' as tz;
import 'package:timezone_finder/browser.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); // required before rootBundle
await tz.initializeTimeZone('packages/timezone/data/latest_all.tzf');
final data = await rootBundle.load(
'packages/timezone_finder/data/boundaries_2026c.bin',
);
installBoundaries(data.buffer.asUint8List());
final myLocation = findLocation(2.3522, 48.8566); // Europe/Paris Location
runApp(const MyApp());
}
Dart web β including Jaspr and other non-Flutter browser apps β has no rootBundle asset pipeline, so fetch the package asset instead. initializeBoundaries is a convenience wrapper around fetch + install; the default packages/β¦ URL depends on <base href> and on your host serving the package tree, so prefer installBoundaries once you have somewhere to put the file:
import 'package:timezone/browser.dart' as tz;
import 'package:timezone_finder/browser.dart';
Future<void> main() async {
await tz.initializeTimeZone('packages/timezone/data/latest_all.tzf');
await initializeBoundaries();
final myLocation = findLocation(2.3522, 48.8566); // Europe/Paris Location
}
Lookups or ensurePreloaded before install throw a StateError naming installBoundaries. A second installBoundaries with the same data version is a no-op; a different version replaces the installed index. Fetch failures throw BoundariesInitException; corrupt bytes throw IndexFormatException.
example/server/ is a Shelf departure-board demo (VM server, not a browser app). The snippets above are the browser init recipe.
Before you start
-
Initialize
latest_all, neverlatest.latestomits the tzdb link identifiers β 341 locations against this dataset's 419 β sofindLocationthrows aStateErrorfor the other 106. The call differs by target, and so does its name:- VM, CLI, server, Flutter mobile and desktop β import
package:timezone/data/latest_all.dart, theninitializeTimeZones()β plural, synchronous, no argument. - Web and Flutter web β import
package:timezone/browser.dart, thenawait initializeTimeZone('packages/timezone/data/latest_all.tzf')β singular, async. The default path fetcheslatest.tzf, so that argument is not optional. Also install boundaries (see Web / Flutter web).
To avoid the fetch and the name difference entirely, the VM form works on web too:
package:timezone/data/latest_all.dartcompiles there andinitializeTimeZones()stays synchronous. It costs about 1.4 MB more JavaScript than fetching the.tzf, which is why fetching is the default advice here. - VM, CLI, server, Flutter mobile and desktop β import
-
Coordinates are longitude first, everywhere.
findLocationandtoLocationtake(longitude, latitude)/ GeoJSON[longitude, latitude]β[2.3522, 48.8566]is Paris. -
Pass one feature, not the whole geocoder response. Nominatim and Photon both answer with a
FeatureCollection, which is several places;toLocationtakes a singleFeatureor a barePointand rejects the collection, because choosing among matches is your decision. -
nullmeans no land polygon covers the point, which is not quite "at sea": coastal zones extend ~22 km offshore, so narrow straits resolve and wide seas do not. It means only that β malformed GeoJSON throwsFormatExceptionand a position off the Earth throwsArgumentError, sonullis never ambiguous.
Good to know
- Boundaries are simplified to ~110 m; random land coordinates disagree with the unsimplified source 0.006 % of the time, more beside enclaves.
- Disputed territories: 25 zone pairs overlap. One identifier is returned by an arbitrary deterministic rule. No answer is a statement about sovereignty.
- The boundary index ships inside the package β that is what buys the offline lookup. The published archive is about 7 MB because both forms ship: base64 chunks for VM/native (embedded at compile time) and a packed
.binfor web (installed at runtime). Each consumer only uses one of them. - Ambiguous and nonexistent wall-clock times are out of scope. On the night a zone springs forward 02:30 does not exist; on the night it falls back it happens twice.
TZDateTimepicks one silently, and this package does not change that β it resolves where a coordinate is, not which of two instants a local time means.
Licences and attribution
- Code β MIT (
LICENSE). - Boundary data β ODbL v1.0 (
LICENSE-DATA). Share-alike: if you publicly ship an adapted database, offer that adapted database under ODbL too. - Metazone strings β Unicode License v3 (
LICENSE-CLDR).
If you redistribute the boundary data (or a work produced from it), include:
Time zone boundary data Β© OpenStreetMap contributors, available under the Open Database License (ODbL). Boundaries built by Timezone Boundary Builder.
IANA publishes no official polygons; tz-link cites TZBB as the de facto source. TZBB is by Evan Siroky, built from OpenStreetMap data.
Libraries
- browser
- Browser / Flutter web entry: install the packed boundary index, then use
the same API as
package:timezone_finder/timezone_finder.dart. - timezone_finder
- Makes
package:timezoneeasier to use: turn a place on Earth into the Location it sits in, convert aTZDateTimefrom one Location to another, and read English metazone labels.