use std::io::{BufWriter, Write};
use std::path::Path;
use s2::cellid::CellID;
use s2::latlng::LatLng;
use super::Result;
use super::admin::AssembledPolygon;
use super::interp::{read_addr_point_mmap, read_node_at};
use super::pass2::SlimInterpWay;
use super::super::format::*;
#[cfg(feature = "test-hooks")]
pub mod test_hooks {
use std::sync::atomic::{AtomicU32, Ordering};
pub static PANIC_AT_STREETS_WAY_IDX: AtomicU32 = AtomicU32::new(u32::MAX);
pub fn reset() {
PANIC_AT_STREETS_WAY_IDX.store(u32::MAX, Ordering::Relaxed);
}
}
pub(super) struct AdminCellEntry {
pub(super) cell_id: u64,
pub(super) poly_index: u32,
pub(super) is_interior: bool,
}
fn cover_segment(
lat1_e7: i32,
lon1_e7: i32,
lat2_e7: i32,
lon2_e7: i32,
level: u8,
mut emit: impl FnMut(u64),
) {
let lat1 = lat1_e7 as f64 * 1e-7;
let lon1 = lon1_e7 as f64 * 1e-7;
let lat2 = lat2_e7 as f64 * 1e-7;
let lon2 = lon2_e7 as f64 * 1e-7;
let c1 = CellID::from(LatLng::from_degrees(lat1, lon1)).parent(level as u64);
let c2 = CellID::from(LatLng::from_degrees(lat2, lon2)).parent(level as u64);
emit(c1.0);
if c1.0 == c2.0 {
return;
}
let dlat = lat2 - lat1;
let dlon = lon2 - lon1;
let seg_len_deg = (dlat.powi(2) + dlon.powi(2)).sqrt();
let step_deg = match level {
17 => 0.0003,
14 => 0.003,
10 => 0.005,
_ => 0.001,
};
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let steps = ((seg_len_deg / step_deg).ceil() as usize).clamp(2, 256);
let mut seen = [0u64; 8];
seen[0] = c1.0;
let mut seen_count = 1usize;
for i in 1..steps {
let t = i as f64 / steps as f64;
let lat = lat1 + t * (lat2 - lat1);
let lon = lon1 + t * (lon2 - lon1);
let c = CellID::from(LatLng::from_degrees(lat, lon))
.parent(level as u64)
.0;
let already = seen[..seen_count].contains(&c);
if !already {
emit(c);
if seen_count < seen.len() {
seen[seen_count] = c;
seen_count += 1;
}
}
}
if !seen[..seen_count].contains(&c2.0) {
emit(c2.0);
}
}
const NUM_BUCKETS: usize = 256;
const STREET_CHUNK: usize = 100_000;
const ADDR_CHUNK: usize = 500_000;
const BUCKET_RECORD_SIZE: usize = 15;
const ENTRY_TYPE_STREET: u8 = 0;
const ENTRY_TYPE_ADDR: u8 = 1;
const ENTRY_TYPE_INTERP: u8 = 2;
fn bucket_for_cell(cell_id: u64) -> usize {
(cell_id >> 56) as usize
}
fn ensure_bucket_writer(
writers: &mut [Option<BufWriter<std::fs::File>>],
bucket: usize,
bucket_dir: &Path,
) -> Result<()> {
if writers[bucket].is_none() {
let path = bucket_dir.join(format!("{bucket:03}"));
writers[bucket] = Some(BufWriter::new(std::fs::File::create(path)?));
}
Ok(())
}
fn write_bucket_record(
w: &mut BufWriter<std::fs::File>,
cell_id: u64,
etype: u8,
index: u32,
segment: u16,
) -> std::io::Result<()> {
let mut buf = [0u8; BUCKET_RECORD_SIZE];
buf[0..8].copy_from_slice(&cell_id.to_le_bytes());
buf[8] = etype;
buf[9..13].copy_from_slice(&index.to_le_bytes());
buf[13..15].copy_from_slice(&segment.to_le_bytes());
w.write_all(&buf)
}
struct ParsedBucketEntry {
cell_id: u64,
etype: u8,
index: u32,
segment: u16,
}
fn parse_bucket_file(data: &[u8]) -> std::io::Result<Vec<ParsedBucketEntry>> {
if !data.len().is_multiple_of(BUCKET_RECORD_SIZE) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"geocode bucket file has partial trailing record ({} bytes, \
not a multiple of {BUCKET_RECORD_SIZE}); Stage A write was \
likely interrupted (ENOSPC or crash)",
data.len(),
),
));
}
let count = data.len() / BUCKET_RECORD_SIZE;
let mut entries = Vec::with_capacity(count);
for i in 0..count {
let off = i * BUCKET_RECORD_SIZE;
let cell_id = u64::from_le_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
]);
let etype = data[off + 8];
let index = u32::from_le_bytes([
data[off + 9],
data[off + 10],
data[off + 11],
data[off + 12],
]);
let segment = u16::from_le_bytes([data[off + 13], data[off + 14]]);
entries.push(ParsedBucketEntry {
cell_id,
etype,
index,
segment,
});
}
Ok(entries)
}
pub(super) struct FusedCellAssignmentParams<'a> {
pub(super) output_dir: &'a Path,
pub(super) street_ways_mmap: &'a [u8],
pub(super) street_nodes_mmap: &'a [u8],
pub(super) street_way_count: u32,
pub(super) addr_points_mmap: &'a [u8],
pub(super) addr_point_count: u32,
pub(super) interp_ways: &'a [SlimInterpWay],
pub(super) interp_nodes_mmap: &'a [u8],
pub(super) fine_level: u8,
pub(super) coarse_level: u8,
pub(super) fine_cells_file: &'a str,
pub(super) fine_street_entries_file: &'a str,
pub(super) fine_addr_entries_file: &'a str,
pub(super) fine_interp_entries_file: &'a str,
pub(super) coarse_cells_file: &'a str,
pub(super) coarse_street_entries_file: &'a str,
pub(super) coarse_addr_entries_file: &'a str,
pub(super) coarse_interp_entries_file: &'a str,
}
#[allow(
clippy::cast_possible_truncation,
clippy::too_many_lines,
clippy::cognitive_complexity
)]
#[hotpath::measure]
pub(super) fn bucketed_cell_assignment_fused(
p: &FusedCellAssignmentParams<'_>,
) -> Result<(u32, u32)> {
use rayon::prelude::*;
let FusedCellAssignmentParams {
output_dir,
street_ways_mmap,
street_nodes_mmap,
street_way_count,
addr_points_mmap,
addr_point_count,
interp_ways,
interp_nodes_mmap,
fine_level,
coarse_level,
fine_cells_file,
fine_street_entries_file,
fine_addr_entries_file,
fine_interp_entries_file,
coarse_cells_file,
coarse_street_entries_file,
coarse_addr_entries_file,
coarse_interp_entries_file,
} = p;
let street_way_count = *street_way_count;
let addr_point_count = *addr_point_count;
let fine_level = *fine_level;
let coarse_level = *coarse_level;
let fine_bucket_dir = output_dir.join(format!(".buckets-level{fine_level}"));
let coarse_bucket_dir = output_dir.join(format!(".buckets-level{coarse_level}"));
for dir in [&fine_bucket_dir, &coarse_bucket_dir] {
if dir.exists() {
std::fs::remove_dir_all(dir)?;
}
std::fs::create_dir_all(dir)?;
}
let fine_guard = crate::path_guard::PathGuard::dir(fine_bucket_dir.clone());
let coarse_guard = crate::path_guard::PathGuard::dir(coarse_bucket_dir.clone());
let mut fine_writers: Vec<Option<BufWriter<std::fs::File>>> =
(0..NUM_BUCKETS).map(|_| None).collect();
let mut coarse_writers: Vec<Option<BufWriter<std::fs::File>>> =
(0..NUM_BUCKETS).map(|_| None).collect();
crate::debug::emit_marker("GEOCODE_PASS3_STAGEA_STREETS_START");
let mut chunk_start = 0u32;
while chunk_start < street_way_count {
let chunk_end = (chunk_start + STREET_CHUNK as u32).min(street_way_count);
#[allow(clippy::type_complexity)]
let entries: Vec<(u64, Option<u64>, u32, u16)> = (chunk_start..chunk_end)
.into_par_iter()
.flat_map_iter(|way_idx| {
#[cfg(feature = "test-hooks")]
if test_hooks::PANIC_AT_STREETS_WAY_IDX.load(std::sync::atomic::Ordering::Relaxed)
== way_idx
{
panic!("test-hooks: geocode pass3 stage A streets way {way_idx} panicking");
}
let offset = way_idx as usize * STREET_WAY_SIZE;
let rec = street_ways_mmap
.get(offset..offset + STREET_WAY_SIZE)
.and_then(|b| <&[u8; STREET_WAY_SIZE]>::try_from(b).ok())
.map(StreetWay::from_bytes);
let mut out = Vec::new();
if let Some(rec) = rec {
let nc = rec.node_count as usize;
if nc >= 2 {
out.reserve(nc * 2);
for seg_idx in 0..nc - 1 {
let off1 = rec.node_offset as usize + seg_idx * NODE_COORD_SIZE;
let off2 = off1 + NODE_COORD_SIZE;
if let (Some(n1), Some(n2)) = (
read_node_at(street_nodes_mmap, off1 as u64),
read_node_at(street_nodes_mmap, off2 as u64),
) {
let mut coarse_seen = [0u64; 4];
let mut coarse_n = 0usize;
cover_segment(n1.0, n1.1, n2.0, n2.1, fine_level, |fine_cid| {
let coarse_cid = CellID(fine_cid).parent(coarse_level as u64).0;
let coarse_to_emit =
if coarse_seen[..coarse_n].contains(&coarse_cid) {
None
} else {
if coarse_n < coarse_seen.len() {
coarse_seen[coarse_n] = coarse_cid;
coarse_n += 1;
}
Some(coarse_cid)
};
out.push((fine_cid, coarse_to_emit, way_idx, seg_idx as u16));
});
}
}
}
}
out.into_iter()
})
.collect();
for &(fine_cid, coarse_opt, wi, si) in &entries {
let b = bucket_for_cell(fine_cid);
ensure_bucket_writer(&mut fine_writers, b, &fine_bucket_dir)?;
write_bucket_record(
fine_writers[b].as_mut().expect("ensured"),
fine_cid,
ENTRY_TYPE_STREET,
wi,
si,
)?;
if let Some(coarse_cid) = coarse_opt {
let b = bucket_for_cell(coarse_cid);
ensure_bucket_writer(&mut coarse_writers, b, &coarse_bucket_dir)?;
write_bucket_record(
coarse_writers[b].as_mut().expect("ensured"),
coarse_cid,
ENTRY_TYPE_STREET,
wi,
si,
)?;
}
}
chunk_start = chunk_end;
}
crate::debug::emit_marker("GEOCODE_PASS3_STAGEA_STREETS_END");
crate::debug::emit_marker("GEOCODE_PASS3_STAGEA_ADDR_START");
let addr_count = addr_point_count as usize;
let mut chunk_start = 0usize;
while chunk_start < addr_count {
let chunk_end = (chunk_start + ADDR_CHUNK).min(addr_count);
let addr_entries: Vec<(u64, u64, u32)> = (chunk_start..chunk_end)
.into_par_iter()
.filter_map(|idx| {
let pt = read_addr_point_mmap(addr_points_mmap, idx as u32)?;
let ll = LatLng::from_degrees(pt.lat_e7 as f64 * 1e-7, pt.lon_e7 as f64 * 1e-7);
let fine_cid = CellID::from(ll).parent(fine_level as u64).0;
let coarse_cid = CellID(fine_cid).parent(coarse_level as u64).0;
Some((fine_cid, coarse_cid, idx as u32))
})
.collect();
for &(fine_cid, coarse_cid, idx) in &addr_entries {
let b = bucket_for_cell(fine_cid);
ensure_bucket_writer(&mut fine_writers, b, &fine_bucket_dir)?;
write_bucket_record(
fine_writers[b].as_mut().expect("ensured"),
fine_cid,
ENTRY_TYPE_ADDR,
idx,
0,
)?;
let b = bucket_for_cell(coarse_cid);
ensure_bucket_writer(&mut coarse_writers, b, &coarse_bucket_dir)?;
write_bucket_record(
coarse_writers[b].as_mut().expect("ensured"),
coarse_cid,
ENTRY_TYPE_ADDR,
idx,
0,
)?;
}
chunk_start = chunk_end;
}
crate::debug::emit_marker("GEOCODE_PASS3_STAGEA_ADDR_END");
crate::debug::emit_marker("GEOCODE_PASS3_STAGEA_INTERP_START");
#[allow(clippy::type_complexity)]
let interp_entries: Vec<(u64, Option<u64>, u32, u16)> = (0..interp_ways.len())
.into_par_iter()
.flat_map_iter(|way_idx| {
let iw = &interp_ways[way_idx];
let nc = iw.node_count as usize;
let mut out = Vec::new();
if nc >= 2 {
out.reserve(nc * 2);
for seg_idx in 0..nc - 1 {
let off1 = iw.node_file_offset as usize + seg_idx * NODE_COORD_SIZE;
let off2 = off1 + NODE_COORD_SIZE;
if let (Some(n1), Some(n2)) = (
read_node_at(interp_nodes_mmap, off1 as u64),
read_node_at(interp_nodes_mmap, off2 as u64),
) {
let mut coarse_seen = [0u64; 4];
let mut coarse_n = 0usize;
cover_segment(n1.0, n1.1, n2.0, n2.1, fine_level, |fine_cid| {
let coarse_cid = CellID(fine_cid).parent(coarse_level as u64).0;
let coarse_to_emit = if coarse_seen[..coarse_n].contains(&coarse_cid) {
None
} else {
if coarse_n < coarse_seen.len() {
coarse_seen[coarse_n] = coarse_cid;
coarse_n += 1;
}
Some(coarse_cid)
};
out.push((fine_cid, coarse_to_emit, way_idx as u32, seg_idx as u16));
});
}
}
}
out.into_iter()
})
.collect();
for &(fine_cid, coarse_opt, wi, si) in &interp_entries {
let b = bucket_for_cell(fine_cid);
ensure_bucket_writer(&mut fine_writers, b, &fine_bucket_dir)?;
write_bucket_record(
fine_writers[b].as_mut().expect("ensured"),
fine_cid,
ENTRY_TYPE_INTERP,
wi,
si,
)?;
if let Some(coarse_cid) = coarse_opt {
let b = bucket_for_cell(coarse_cid);
ensure_bucket_writer(&mut coarse_writers, b, &coarse_bucket_dir)?;
write_bucket_record(
coarse_writers[b].as_mut().expect("ensured"),
coarse_cid,
ENTRY_TYPE_INTERP,
wi,
si,
)?;
}
}
crate::debug::emit_marker("GEOCODE_PASS3_STAGEA_INTERP_END");
for writer in fine_writers.iter_mut().flatten() {
writer.flush()?;
}
for writer in coarse_writers.iter_mut().flatten() {
writer.flush()?;
}
drop(fine_writers);
drop(coarse_writers);
crate::debug::emit_marker("GEOCODE_PASS3_STAGEB_FINE_START");
let fine_count = run_stage_b(
output_dir,
&fine_bucket_dir,
fine_cells_file,
fine_street_entries_file,
fine_addr_entries_file,
fine_interp_entries_file,
)?;
drop(fine_guard);
crate::debug::emit_marker("GEOCODE_PASS3_STAGEB_FINE_END");
crate::debug::emit_marker("GEOCODE_PASS3_STAGEB_COARSE_START");
let coarse_count = run_stage_b(
output_dir,
&coarse_bucket_dir,
coarse_cells_file,
coarse_street_entries_file,
coarse_addr_entries_file,
coarse_interp_entries_file,
)?;
drop(coarse_guard);
crate::debug::emit_marker("GEOCODE_PASS3_STAGEB_COARSE_END");
Ok((fine_count, coarse_count))
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
fn run_stage_b(
output_dir: &Path,
bucket_dir: &Path,
cells_file: &str,
street_entries_file: &str,
addr_entries_file: &str,
interp_entries_file: &str,
) -> Result<u32> {
use rayon::prelude::*;
let mut cells_out = BufWriter::new(std::fs::File::create(output_dir.join(cells_file))?);
let mut street_out =
BufWriter::new(std::fs::File::create(output_dir.join(street_entries_file))?);
let mut addr_out = BufWriter::new(std::fs::File::create(output_dir.join(addr_entries_file))?);
let mut interp_out =
BufWriter::new(std::fs::File::create(output_dir.join(interp_entries_file))?);
let mut street_byte_offset: u64 = 0;
let mut addr_byte_offset: u64 = 0;
let mut interp_byte_offset: u64 = 0;
let mut total_cells: u32 = 0;
let mut prev_cell_id: u64 = 0;
let sorted_buckets: Vec<Vec<ParsedBucketEntry>> = (0..NUM_BUCKETS)
.into_par_iter()
.map(|bucket_idx| -> std::io::Result<Vec<ParsedBucketEntry>> {
let bucket_path = bucket_dir.join(format!("{bucket_idx:03}"));
if !bucket_path.exists() {
return Ok(Vec::new());
}
let data = std::fs::read(&bucket_path)?;
std::fs::remove_file(&bucket_path)?;
if data.is_empty() {
return Ok(Vec::new());
}
let mut entries = parse_bucket_file(&data)?;
drop(data);
entries.sort_unstable_by_key(|e| e.cell_id);
Ok(entries)
})
.collect::<std::io::Result<_>>()?;
for entries in &sorted_buckets {
if entries.is_empty() {
continue;
}
let mut i = 0;
let mut streets: Vec<&ParsedBucketEntry> = Vec::new();
let mut addrs: Vec<&ParsedBucketEntry> = Vec::new();
let mut interps: Vec<&ParsedBucketEntry> = Vec::new();
while i < entries.len() {
let cell_id = entries[i].cell_id;
let group_start = i;
while i < entries.len() && entries[i].cell_id == cell_id {
i += 1;
}
let group = &entries[group_start..i];
streets.clear();
addrs.clear();
interps.clear();
for e in group {
match e.etype {
ENTRY_TYPE_STREET => streets.push(e),
ENTRY_TYPE_ADDR => addrs.push(e),
ENTRY_TYPE_INTERP => interps.push(e),
_ => {}
}
}
let has_streets = !streets.is_empty();
if has_streets {
let count = u16::try_from(streets.len()).map_err(|_| {
format!(
"geocode Stage B: cell {cell_id} has {} street entries, exceeds u16::MAX. \
Bump on-disk count to u32 and increment FORMAT_VERSION.",
streets.len()
)
})?;
street_out.write_all(&count.to_le_bytes())?;
for e in &streets {
street_out.write_all(
&SegmentRef {
way_index: e.index,
segment_index: e.segment,
}
.to_bytes(),
)?;
}
}
let has_addrs = !addrs.is_empty();
if has_addrs {
let count = u16::try_from(addrs.len()).map_err(|_| {
format!(
"geocode Stage B: cell {cell_id} has {} addr entries, exceeds u16::MAX. \
Bump on-disk count to u32 and increment FORMAT_VERSION.",
addrs.len()
)
})?;
addr_out.write_all(&count.to_le_bytes())?;
for e in &addrs {
addr_out.write_all(&e.index.to_le_bytes())?;
}
}
let has_interps = !interps.is_empty();
if has_interps {
let count = u16::try_from(interps.len()).map_err(|_| {
format!(
"geocode Stage B: cell {cell_id} has {} interp entries, exceeds u16::MAX. \
Bump on-disk count to u32 and increment FORMAT_VERSION.",
interps.len()
)
})?;
interp_out.write_all(&count.to_le_bytes())?;
for e in &interps {
interp_out.write_all(
&SegmentRef {
way_index: e.index,
segment_index: e.segment,
}
.to_bytes(),
)?;
}
}
let gc = GeoCell {
cell_id,
street_offset: if has_streets {
street_byte_offset
} else {
NO_DATA_U64
},
addr_offset: if has_addrs {
addr_byte_offset
} else {
NO_DATA_U64
},
interp_offset: if has_interps {
interp_byte_offset
} else {
NO_DATA_U64
},
};
cells_out.write_all(&gc.to_bytes())?;
debug_assert!(
cell_id > prev_cell_id || total_cells == 0,
"bucket ordering violated: cell {cell_id} <= prev {prev_cell_id}"
);
prev_cell_id = cell_id;
total_cells += 1;
if has_streets {
street_byte_offset += 2 + (streets.len() * SEGMENT_REF_SIZE) as u64;
}
if has_addrs {
addr_byte_offset += 2 + (addrs.len() * 4) as u64;
}
if has_interps {
interp_byte_offset += 2 + (interps.len() * SEGMENT_REF_SIZE) as u64;
}
}
}
cells_out.flush()?;
street_out.flush()?;
addr_out.flush()?;
interp_out.flush()?;
Ok(total_cells)
}
#[allow(clippy::cast_possible_truncation)]
#[hotpath::measure]
pub(super) fn assign_admin_cells(
polygons: &[AssembledPolygon],
admin_level: u8,
) -> Vec<AdminCellEntry> {
use rayon::prelude::*;
polygons
.par_iter()
.enumerate()
.flat_map_iter(|(poly_idx, poly)| {
admin_cells_for_polygon(poly_idx, poly, admin_level).into_iter()
})
.collect()
}
#[allow(clippy::cast_possible_truncation)]
fn admin_cells_for_polygon(
poly_idx: usize,
poly: &AssembledPolygon,
admin_level: u8,
) -> Vec<AdminCellEntry> {
let (ext_f64, hole_rings) = parse_polygon_rings(&poly.vertices);
if ext_f64.len() < 3 {
return Vec::new();
}
let hole_slices: Vec<&[(f64, f64)]> = hole_rings.iter().map(Vec::as_slice).collect();
let mut edge_cells = rustc_hash::FxHashSet::default();
for v in poly.vertices.windows(2) {
if v[0] == RING_SENTINEL || v[1] == RING_SENTINEL {
continue;
}
cover_segment(
v[0].lat_e7,
v[0].lon_e7,
v[1].lat_e7,
v[1].lon_e7,
admin_level,
|cid| {
edge_cells.insert(cid);
},
);
}
let mut entries: Vec<AdminCellEntry> = edge_cells
.iter()
.map(|&cid| AdminCellEntry {
cell_id: cid,
poly_index: poly_idx as u32,
is_interior: false,
})
.collect();
let exterior_end = poly
.vertices
.iter()
.position(|v| *v == RING_SENTINEL)
.unwrap_or(poly.vertices.len());
let exterior = &poly.vertices[..exterior_end];
if exterior.len() < 3 {
return entries;
}
let (sum_lat, sum_lon, count) = exterior.iter().fold((0i64, 0i64, 0i64), |(sl, sn, c), v| {
(sl + v.lat_e7 as i64, sn + v.lon_e7 as i64, c + 1)
});
if count == 0 {
return entries;
}
let clat = sum_lat as f64 / count as f64 * 1e-7;
let clon = sum_lon as f64 / count as f64 * 1e-7;
if !crate::geo::point_in_polygon(clon, clat, &ext_f64, &hole_slices) {
return entries;
}
let seed = CellID::from(LatLng::from_degrees(clat, clon)).parent(admin_level as u64);
let mut visited = rustc_hash::FxHashSet::default();
let mut queue = std::collections::VecDeque::new();
visited.insert(seed.0);
queue.push_back(seed);
while let Some(cell) = queue.pop_front() {
if edge_cells.contains(&cell.0) {
continue;
}
let center_ll = s2::latlng::LatLng::from(cell);
if crate::geo::point_in_polygon(
center_ll.lng.deg(),
center_ll.lat.deg(),
&ext_f64,
&hole_slices,
) {
entries.push(AdminCellEntry {
cell_id: cell.0,
poly_index: poly_idx as u32,
is_interior: true,
});
for n in cell.all_neighbors(admin_level as u64) {
if !visited.contains(&n.0) {
visited.insert(n.0);
queue.push_back(n);
}
}
}
}
entries
}
#[allow(clippy::type_complexity)]
fn parse_polygon_rings(vertices: &[NodeCoord]) -> (Vec<(f64, f64)>, Vec<Vec<(f64, f64)>>) {
super::super::format::parse_polygon_rings(vertices.iter().copied())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cover_segment_same_cell() {
let lat1 = 556_762_000; let lon1 = 125_683_000; let lat2 = 556_762_100; let lon2 = 125_683_100;
let mut cells = Vec::new();
cover_segment(lat1, lon1, lat2, lon2, 17, |c| cells.push(c));
assert_eq!(
cells.len(),
1,
"very close points should produce exactly 1 cell"
);
}
#[test]
fn cover_segment_cross_cell() {
let lat1 = 556_700_000; let lon1 = 125_600_000; let lat2 = 556_800_000; let lon2 = 125_700_000;
let mut cells = Vec::new();
cover_segment(lat1, lon1, lat2, lon2, 17, |c| cells.push(c));
assert!(
cells.len() >= 2,
"points ~1km apart should cross at least 2 level-17 cells, got {}",
cells.len()
);
}
#[test]
fn cover_segment_endpoints_included() {
let lat1 = 556_700_000;
let lon1 = 125_600_000;
let lat2 = 556_800_000;
let lon2 = 125_700_000;
let c1 = CellID::from(LatLng::from_degrees(55.6700, 12.5600))
.parent(17)
.0;
let c2 = CellID::from(LatLng::from_degrees(55.6800, 12.5700))
.parent(17)
.0;
let mut cells = Vec::new();
cover_segment(lat1, lon1, lat2, lon2, 17, |c| cells.push(c));
assert!(cells.contains(&c1), "first endpoint cell must be included");
assert!(cells.contains(&c2), "second endpoint cell must be included");
}
}