osm-katana/src/geometry.rs
#![allow(clippy::float_arithmetic, clippy::arithmetic_side_effects)]
/// Tags whose mere PRESENCE implies a closed way is a filled area.
///
/// **`"area"` is deliberately not in this list.** It used to be, and that made the
/// explicit `area == "yes"` test below dead code — `area` matched here whatever its
/// value, so `area=no`, OSM's one way of saying *this closed way is a line, not a
/// filled shape*, produced a Polygon. See [`area_verdict`].
const AREA_TAGS: &[&str] = &["building", "landuse", "natural", "amenity", "leisure"];
/// What an explicit `area=…` value says, if anything.
///
/// `area` is a yes/no key, so a value is one of three things and not two: an
/// affirmation, a denial, or noise. MEASURED on korp's 2026-08-03 `sweden` cut, over
/// 9 204 689 ways: 16 176 `area=yes`, 505 `area=no`, and 14 values that are neither
/// (`highway`, `yess`, `0.4`, `1 070`, …). Noise must not be read as either verdict —
/// it has to fall through to [`AREA_TAGS`], because a way tagged
/// `building=yes` + `area=vehicletesting` is still a building.
#[inline]
fn area_verdict(v: &str) -> Option<bool> {
match v {
"yes" | "true" | "1" => Some(true),
"no" | "false" | "0" => Some(false),
_ => None,
}
}
/// Encode a WKB Point on the stack — exactly 21 bytes, no heap allocation.
/// This is called 9.5 billion times for planet; the stack array avoids
/// that many malloc/free cycles.
#[inline]
pub fn encode_point_inline(lon: f64, lat: f64) -> [u8; 21] {
let mut buf = [0u8; 21];
buf[0] = 1; // little-endian
buf[1..5].copy_from_slice(&1u32.to_le_bytes()); // WKB Point type
buf[5..13].copy_from_slice(&lon.to_le_bytes());
buf[13..21].copy_from_slice(&lat.to_le_bytes());
buf
}
/// Is this closed way an area (polygon) rather than a ring-shaped line?
///
/// Takes the tags as the **borrowed pairs both call sites already hold** —
/// exactly the shape [`crate::tags::serialize_tags`] takes, so a way's tags are
/// read twice and copied zero times. This used to take a
/// `HashMap<String, String>`, which meant every caller built a throwaway map,
/// with two `to_string()` per tag, to answer three lookups: 8 594 851 maps per
/// Sweden convert, inside the gatling workers where memory bandwidth is the
/// shared resource (stinky `katana-5`).
///
/// Generic over the pair types so the PBF path can pass string-table slices
/// (`&str`) and the XML path its already-owned `String`s, without either side
/// converting.
///
/// Duplicate-key semantics are the `HashMap`'s: **last wins**. That only shows
/// up on a way tagged `area` twice with different values, which OSM does not
/// permit, but matching it exactly keeps the change byte-for-byte invisible in
/// the output.
///
/// ## `area=no` is honoured, and it OVERRIDES
///
/// An explicit `area` verdict wins over every implied one: `area=no` on a closed
/// `building=yes` way means the mapper is saying that ring is a line. That is the whole
/// point of the key — it exists only to contradict what the other tags imply, so a
/// version that lets `building` outvote it has no effect at all.
///
/// This decides the WKB the encoder writes — `true` ⇒ Polygon, `false` ⇒ LineString —
/// so it moves geometry in every published pack. MEASURED on korp's 2026-08-03 `sweden`
/// cut: **279 of 9 204 689 ways (0,0030 %)** are closed and tagged `area=no` and were
/// therefore shipped as Polygons; they now ship as LineStrings. `area=yes` is unchanged
/// (16 176 ways), and so is every way with no `area` key at all — 9 187 994 of them.
pub fn is_closed_area<K: AsRef<str>, V: AsRef<str>>(refs: &[i64], tags: &[(K, V)]) -> bool {
if refs.len() < 4 {
return false;
}
if refs.first() != refs.last() {
return false;
}
// An explicit, INTELLIGIBLE `area` value is the answer — in both directions.
// An unintelligible one (`area=0.4`) says nothing and falls through.
if let Some(verdict) = tags
.iter()
.rev()
.find(|(k, _)| k.as_ref() == "area")
.and_then(|(_, v)| area_verdict(v.as_ref()))
{
return verdict;
}
tags.iter().any(|(k, _)| AREA_TAGS.contains(&k.as_ref()))
}
#[cfg(test)]
mod tests {
use super::is_closed_area;
const CLOSED: [i64; 4] = [1, 2, 3, 1];
#[test]
fn open_or_short_ways_are_never_areas() {
assert!(!is_closed_area(&[1, 2, 3, 4], &[("building", "yes")]));
assert!(!is_closed_area(&[1, 2, 1], &[("building", "yes")]));
}
#[test]
fn an_area_tag_makes_a_closed_way_an_area() {
assert!(is_closed_area(&CLOSED, &[("area", "yes")]));
assert!(is_closed_area(&CLOSED, &[("landuse", "forest")]));
assert!(!is_closed_area(&CLOSED, &[("highway", "residential")]));
assert!(!is_closed_area(&CLOSED, &[] as &[(&str, &str)]));
}
/// **`area=no` means NOT an area — stinky `katana-10`, fixed.**
///
/// This test used to assert the opposite and called itself
/// `area_no_is_still_treated_as_an_area_quirk_preserved`: `"area"` was itself
/// in `AREA_TAGS`, so the key matched whatever its value and the explicit
/// `area == "yes"` branch was unreachable. A closed way tagged `area=no` was
/// shipped as a Polygon.
///
/// MEASURED on korp's 2026-08-03 `sweden` cut: 279 of 9 204 689 ways.
///
/// Every assertion here is one the OLD implementation fails, and the last two
/// are the ones that matter: the verdict must OVERRIDE the implied tags, in
/// both directions, or the key does nothing.
#[test]
fn area_no_means_not_an_area() {
assert!(!is_closed_area(&CLOSED, &[("area", "no")]));
// Last wins, as it always did — but now the value is read.
assert!(!is_closed_area(&CLOSED, &[("area", "yes"), ("area", "no")]));
assert!(is_closed_area(&CLOSED, &[("area", "no"), ("area", "yes")]));
// The override, which is the entire purpose of the key: an implied area
// tag does not outvote an explicit denial…
assert!(
!is_closed_area(&CLOSED, &[("building", "yes"), ("area", "no")]),
"area=no must override an implied area tag — otherwise the key is inert"
);
assert!(!is_closed_area(
&CLOSED,
&[("landuse", "forest"), ("area", "false")]
));
// …and an explicit affirmation still stands on its own.
assert!(is_closed_area(
&CLOSED,
&[("highway", "pedestrian"), ("area", "yes")]
));
}
/// A value that is neither yes nor no carries no verdict, so the other tags
/// decide. MEASURED on the same cut: 14 ways carry such a value (`highway`,
/// `yess`, `vehicletesting`, `0.4`, `1 070`, …). Reading them as "no" would
/// silently unfill real buildings; reading them as "yes" is the old bug.
#[test]
fn an_unintelligible_area_value_decides_nothing() {
assert!(is_closed_area(
&CLOSED,
&[("building", "yes"), ("area", "vehicletesting")]
));
assert!(!is_closed_area(
&CLOSED,
&[("highway", "residential"), ("area", "0.4")]
));
// `area` alone, with junk, is no longer an area tag by mere presence.
assert!(!is_closed_area(&CLOSED, &[("area", "yess")]));
}
/// **The APPLIED output, not the predicate.** `is_closed_area` only matters
/// because it picks a WKB type byte, so this drives the real PBF parser and
/// the real encoder ([`crate::shared::gpu_backend::CpuBackend::encode_wkb`])
/// over a real synthetic PBF and reads the type byte off the produced bytes.
///
/// WKB type 2 = LineString, 3 = Polygon. Three closed ways over the same four
/// nodes, differing only in tags: `building=yes` (Polygon), the same plus
/// `area=no` (LineString), and `area=yes` on a highway (Polygon).
#[test]
fn the_encoder_writes_a_linestring_for_area_no() {
use crate::pbf_enc;
use crate::shared::gpu_backend::{CpuBackend, GpuBackend as _, RawWay};
/// WKB geometry type of a little-endian body: bytes 1..5.
fn wkb_type(g: &[u8]) -> u32 {
u32::from_le_bytes(g[1..5].try_into().expect("wkb shorter than its header"))
}
let mut st = pbf_enc::StringTable::new();
let sid = |st: &mut pbf_enc::StringTable, s: &str| st.intern(s.as_bytes());
let (k_building, v_yes) = (sid(&mut st, "building"), sid(&mut st, "yes"));
let (k_area, v_no) = (sid(&mut st, "area"), sid(&mut st, "no"));
let (k_highway, v_ped) = (sid(&mut st, "highway"), sid(&mut st, "pedestrian"));
let v_yes2 = sid(&mut st, "yes");
let mut dn = pbf_enc::DenseNodesBuilder::new();
for id in 1..=4i64 {
dn.push(id, 100_000 * id as i32, 200_000 * id as i32, &[]);
}
let ring = [1i64, 2, 3, 1];
let ways = vec![
pbf_enc::encode_way(10, &[(k_building, v_yes)], &ring),
pbf_enc::encode_way(11, &[(k_building, v_yes), (k_area, v_no)], &ring),
pbf_enc::encode_way(12, &[(k_highway, v_ped), (k_area, v_yes2)], &ring),
];
let block = pbf_enc::encode_primitive_block(&st, Some(&dn), &ways, &[]);
let data = pbf_enc::fixtures::pbf_file(&[block]);
let positions = crate::pbf_io::scan_blob_positions_par(&data);
let store = crate::reader::read_pass1_mmap(&data, &positions).unwrap();
let (par_ways, _rels, _loss) =
crate::reader::read_pass2_mmap(&data, &positions, &store, &CpuBackend).unwrap();
let mut by_id: Vec<(i64, u32)> = par_ways
.iter()
.map(|w| {
let g = w.geometry.as_ref().unwrap_or_else(|| {
panic!(
"way {} produced no geometry — the fixture never reached the encoder",
w.id
)
});
(w.id, wkb_type(g))
})
.collect();
by_id.sort_unstable();
assert_eq!(
by_id,
vec![(10, 3u32), (11, 2u32), (12, 3u32)],
"building=yes ⇒ Polygon(3); the SAME way plus area=no ⇒ LineString(2); \
highway=pedestrian + area=yes ⇒ Polygon(3)"
);
// The instrument is alive: the encoder really can produce both bytes off
// this same ring, so a single-value result above would be a dead test.
let coords: Vec<(f64, f64)> = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)];
let both = CpuBackend.encode_wkb(&[
RawWay {
coords: coords.clone(),
is_area: true,
},
RawWay {
coords,
is_area: false,
},
]);
assert_eq!(
both.iter()
.map(|g| wkb_type(g.as_ref().unwrap()))
.collect::<Vec<_>>(),
vec![3u32, 2u32]
);
}
/// Owned and borrowed tag lists must give the same answer — the two call
/// sites hold different types and there is only one implementation.
#[test]
fn owned_and_borrowed_tags_agree() {
let owned: Vec<(String, String)> = vec![(String::from("natural"), String::from("water"))];
let borrowed: Vec<(&str, &str)> = vec![("natural", "water")];
assert_eq!(
is_closed_area(&CLOSED, &owned),
is_closed_area(&CLOSED, &borrowed)
);
assert!(is_closed_area(&CLOSED, &owned));
}
}