AI authored to showcase ironpad capabilities.
"What timezone is this point in?" is a computational geometry problem. There is no table to index. A longitude and latitude land somewhere inside one of a set of irregular polygons that tile the whole planet, oceans included, and answering means deciding which polygon contains the point. Some of those polygons wrap most of a hemisphere; some are slivers a few kilometers wide; a few are open water with no country attached at all.
I wrote rtz to make that question boring to answer. It is one Rust crate that ships as four things from a single codebase: a command-line binary, a library, a web server (there is a free one running in four regions at tz.twitchax.com), and a WebAssembly module. It carries its own map data baked into the binary, so there is no database to stand up and no network call on the hot path. About 175 GitHub stars later, it turns out a lot of people share the same timezone pain.
The cells below run the real crate, with its real embedded dataset, compiled to wasm in your browser. Nothing is mocked. When a cell resolves Kathmandu to a :45 offset, that is rtz 0.7.3 doing a point-in-polygon test against Natural Earth's timezone geometry, right here on this page. Let's ask it some questions.
The core call is NedTimezone::lookup(lng, lat). You hand it a longitude and a latitude as f32, and it hands back the timezones whose polygons contain that point. The return is a Vec because the world is messy: overlapping territorial claims can put a single coordinate in more than one zone, and a point in the deep ocean can be in none.
The table below is a small world tour, chosen to show off the parts of timezones people forget exist. Not every offset is a whole number of hours. Nepal runs at +05:45. India and Sri Lanka sit at +05:30. The Marquesas Islands are at -09:30. And the open Pacific has real, named nautical zones with a genuine UTC offset but no IANA identifier at all, because no country owns the water.
A resolved zone is more than a name. Natural Earth's timezone features carry a small bundle of fields, and rtz exposes them straight off the NedTimezone struct. The two that matter most are the human-readable identifier (an IANA name like Asia/Kathmandu, when one exists) and the offset (a display string like UTC+05:45). Alongside those you get raw_offset in whole seconds (handy for arithmetic: 20700 seconds is five hours and forty-five minutes), the numeric zone as a signed float, a description listing the countries the zone covers, and a dst_description for daylight-saving notes.
Keep both offset fields in mind, because they answer different questions. The offset string is what you print for a human; the raw_offset in seconds is what you add to a UTC timestamp to get local wall-clock time, no parsing required. For a mental model, offset is the label on the clock and raw_offset is the number of seconds you turn the hands.
One wrinkle, early: identifier is an Option. For the nautical ocean zones there is no IANA name to hand back, so it comes back empty even though the offset is perfectly real. The next cell pulls the whole record for a single point so you can see every field at once.
Everything above came out of data compiled directly into the wasm module running this page. No fetch, no CDN, no API key. That is the self-contained feature doing its job, and it is the whole reason rtz works the same in a CLI, on a server, and in a browser: the map travels with the code.
For the Natural Earth dataset, "the map" is two binary blobs. The timezone geometry itself is about 763 KB, and a precomputed lookup cache adds about 1.1 MB, for roughly 1.9 MB riding inside the cell. In exchange for that weight, every lookup is a pure in-memory computation.
The geometry is deliberately coarse. Natural Earth simplifies the whole planet down to 120 timezone polygons, essentially offset bands with the political boundaries smoothed off (more on that honesty later). The lookup cache is a hash map from every one-degree cell of the globe, all 64,800 of them (360 longitudes by 180 latitudes), to the list of zone polygons that touch that cell. The next cell reads those numbers straight out of the live in-memory structures.
A timezone polygon is just a ring of longitude-latitude vertices, with holes punched in it now and then. Natural Earth's are mostly the familiar staircase shapes: vertical offset bands that jog east or west to follow a coastline or a national border, then run straight through open ocean.
You can see the whole structure by turning the question around. Instead of asking about one point, sample a grid of points across the entire globe, resolve each one, and color it by its UTC offset. The cell below does exactly that: it walks a 180-by-90 grid of coordinates, calls NedTimezone::lookup on each, and paints the result, warm hues east of Greenwich and cool hues west, with anything it cannot place left dark.
What comes back is the classic timezone map, drawn entirely by asking rtz the same question sixteen thousand times. The vertical bands are the offset zones. Where they wiggle, that is a boundary following a coast or a border in the underlying geometry. Where they run arrow-straight, that is a nautical zone slicing the ocean into fifteen-degree lanes. Every block is a real point-in-polygon test against the embedded data.
The naive way to answer "which polygon contains this point" is to test the point against every polygon until one hits. For 120 zones that is up to 120 point-in-polygon tests per query, and point-in-polygon on a many-vertex ring is not free. It works, but it scales with the size of the dataset, and the OSM dataset rtz also supports has far more than 120 zones.
rtz sidesteps almost all of that work with the lookup cache from the previous section. Round the query down to its one-degree cell, look that cell up in the hash map, and you get back only the handful of polygons that actually intersect that cell. Then you run point-in-polygon on just those candidates. Across all 64,800 cells there are only 72,791 candidate entries total, so the average cell lists about 1.12 candidates, and the busiest cell on the planet lists just five. A scan that was 120 tests collapses to roughly one.
This has a name: a spatial index, the same idea as an R-tree or a quadtree, flattened into a flat array keyed by integer degrees because the whole planet fits in 360 by 180 cells. The one-time cost is the 1.1 MB the cache weighs, and every lookup after that skips straight to a short candidate list.
The README benchmark puts real numbers on it: the cache improves average resolution by about 96x, and a typical NED lookup lands around 460 nanoseconds. The cell below runs both paths against the same set of points, times them with a stopwatch (directional rather than precise, since your browser is timing wasm), names the work each one does, and confirms the two paths return byte-identical answers.
rtz ships two timezone datasets, and the cells here use the smaller one. Natural Earth (NED) is the 1.9 MB set you have been querying: 120 coarse offset bands, fast and tiny, excellent when you mostly care about the UTC offset. OpenStreetMap (OSM) is the other option, roughly 8 MB, with true political boundaries and much finer detail, at the cost of the extra weight. Same API, heavier map.
Because the Natural Earth zones are offset bands, the identifier you get back is a representative IANA name for that band, not necessarily the one you would reach for. Query the point (30, 30), which is in Egypt, and NED reports the UTC+02:00 offset correctly but labels it Europe/Mariehamn, the first name that band happens to carry. The description field still lists Egypt; the offset is still correct; only the friendly label is a stand-in. The ocean zones, as we saw, carry no identifier at all.
There is also geometric simplification underneath. Natural Earth's boundaries are smoothed to a small epsilon, so a point in a border town a few hundred meters from a line can land on the wrong side. If you need a query near a border to be exactly right, that is what the OSM dataset and the full server build are for. NED trades a little accuracy for a tiny, fast, self-contained answer, and the trade is stated plainly.
That is rtz: a binary you can cargo install and query from the shell, a library you drop into a Rust project, a server that answers eight thousand requests a second per region, and a WebAssembly module. The npm package rtzweb is that last one, and the crate's own website runs exactly the code these cells just ran, in exactly this way, in the browser. You have been talking to the real thing the whole time.
The data comes from Natural Earth and OpenStreetMap; the speed comes from baking the map into the binary and pre-bucketing the world into one-degree cells; the reach comes from Rust compiling cleanly to all four targets from one source. If you want the full tour, the code and the datasets are at github.com/twitchax/rtz.
These cells run rtz 0.7.3, published today, after gently un-sticking the crate from a pre-release bincode git pin and teaching self-contained to actually ship its map data in the package instead of rebuilding it from source geometry. The timezones, thankfully, did not move.