1use std::{cmp::Ordering, collections::HashMap, io::Write, sync::Arc};
24
25use anyhow::{Context, Result, bail};
26use geojson::GeometryValue;
27use h3o::{CellIndex, LatLng, Resolution};
28use serde::Serialize;
29
30use crate::{
31 container::{Bytes, Container, ContainerWriter},
32 extsort::{ExternalSorter, Scratch, SortStats, Spill},
33 memory::MemoryBudget,
34 pack::RecordId,
35 record::{Layer, Record},
36 records::{RecordsReader, dequantize},
37 util::codec::{get_u64, put_u64, read_u64_le},
38};
39
40pub const SPATIAL_VERSION: u32 = 2;
41const FINE: &str = "spatial/fine";
42const CONTEXT: &str = "spatial/context";
43
44const H3_FINE_RESOLUTION: Resolution = Resolution::Eleven;
45const H3_CONTEXT_RESOLUTION: Resolution = Resolution::Six;
46const H3_SEGMENT_SAMPLE_DIVISOR: f64 = 2.0;
47const H3_RADIUS_EXTRA_RING: u32 = 1;
48const H3_MAX_QUERY_K: u32 = 128;
49const EARTH_RADIUS_M: f64 = 6_371_008.8;
50const CELLS_PER_BLOCK: u64 = 16;
51const SEGMENT_INDEX_BITS: u32 = 16;
52const KIND_POINT: u8 = 0;
53const KIND_SEGMENT: u8 = 1;
54
55#[derive(Debug, Clone, Serialize, PartialEq)]
56pub struct PointCandidate {
57 pub record_id: RecordId,
58 pub layer: Layer,
59 pub lon: f64,
60 pub lat: f64,
61 pub distance_m: f64,
62}
63
64#[derive(Debug, Clone, Serialize, PartialEq)]
65pub struct SegmentCandidate {
66 pub record_id: RecordId,
67 pub layer: Layer,
68 pub closest_lon: f64,
69 pub closest_lat: f64,
70 pub distance_m: f64,
71 pub fraction: f64,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
76pub struct CellRef {
77 cell: u64,
78 kind: u8,
79 reference: u64,
80}
81
82impl Spill for CellRef {
83 fn encode(&self, out: &mut Vec<u8>) {
84 (self.cell, self.kind, self.reference).encode(out);
85 }
86
87 fn decode(input: &mut &[u8]) -> Result<Self> {
88 let (cell, kind, reference) = <(u64, u8, u64)>::decode(input)?;
89 Ok(Self {
90 cell,
91 kind,
92 reference,
93 })
94 }
95}
96
97#[derive(Debug, Default)]
99pub struct RecordCells {
100 fine: Vec<CellRef>,
101 context: Vec<CellRef>,
102 points: u64,
103 segments: u64,
104}
105
106impl RecordCells {
107 pub fn for_record(record_id: RecordId, record: &Record) -> Result<Self> {
108 let mut cells = Self::default();
109 match record {
110 Record::Street(_) | Record::Interpolation(_) => {
111 let GeometryValue::LineString { coordinates } = &record.geometry().value else {
112 return Ok(cells);
113 };
114 let positions = coordinates
115 .iter()
116 .filter_map(|position| match position.as_slice() {
117 [lon, lat, ..] if lon.is_finite() && lat.is_finite() => Some([*lon, *lat]),
118 _ => None,
119 })
120 .collect::<Vec<_>>();
121 for (index, pair) in positions.windows(2).enumerate() {
122 if haversine_m(pair[0][0], pair[0][1], pair[1][0], pair[1][1]) <= f64::EPSILON {
123 continue;
124 }
125 let index = u64::try_from(index)?;
126 if index >= 1 << SEGMENT_INDEX_BITS {
127 bail!("line record {} has too many segments to index", record.id());
128 }
129 let reference = record_id << SEGMENT_INDEX_BITS | index;
130 for cell in segment_cells(pair[0], pair[1], H3_FINE_RESOLUTION)? {
131 cells.fine.push(CellRef {
132 cell,
133 kind: KIND_SEGMENT,
134 reference,
135 });
136 }
137 cells.segments += 1;
138 }
139 }
140 _ => {
141 let Some([lon, lat]) = record.display_point() else {
142 return Ok(cells);
143 };
144 cells.fine.push(CellRef {
145 cell: cell_id(lon, lat, H3_FINE_RESOLUTION)?,
146 kind: KIND_POINT,
147 reference: record_id,
148 });
149 if record.layer().is_context() {
150 cells.context.push(CellRef {
151 cell: cell_id(lon, lat, H3_CONTEXT_RESOLUTION)?,
152 kind: KIND_POINT,
153 reference: record_id,
154 });
155 }
156 cells.points += 1;
157 }
158 }
159 Ok(cells)
160 }
161}
162
163#[derive(Debug, Clone, Default, PartialEq, Eq)]
164pub struct SpatialCommit {
165 pub point_count: u64,
166 pub segment_count: u64,
167 pub cell_count: u64,
168 pub context_cell_count: u64,
169 pub fine_pairs: SortStats,
170 pub context_pairs: SortStats,
171}
172
173pub struct SpatialIndexWriter {
174 fine: ExternalSorter<CellRef>,
175 context: ExternalSorter<CellRef>,
176 points: u64,
177 segments: u64,
178}
179
180impl SpatialIndexWriter {
181 pub fn new(scratch: &Arc<Scratch>, budget: &Arc<MemoryBudget>, share_bytes: usize) -> Self {
182 Self {
183 fine: ExternalSorter::new(scratch, "spatial-fine", budget, share_bytes),
184 context: ExternalSorter::new(scratch, "spatial-context", budget, share_bytes / 8),
185 points: 0,
186 segments: 0,
187 }
188 }
189
190 pub fn add(&mut self, cells: RecordCells) -> Result<()> {
191 for pair in cells.fine {
192 self.fine.push(pair)?;
193 }
194 for pair in cells.context {
195 self.context.push(pair)?;
196 }
197 self.points += cells.points;
198 self.segments += cells.segments;
199 Ok(())
200 }
201
202 pub fn finish(self, pack: &mut ContainerWriter) -> Result<SpatialCommit> {
203 let fine_pairs = self.fine.stats();
204 let context_pairs = self.context.stats();
205 let cell_count = write_cell_index(pack, FINE, self.fine)?;
206 let context_cell_count = write_cell_index(pack, CONTEXT, self.context)?;
207 Ok(SpatialCommit {
208 point_count: self.points,
209 segment_count: self.segments,
210 cell_count,
211 context_cell_count,
212 fine_pairs,
213 context_pairs,
214 })
215 }
216}
217
218fn write_cell_index(
219 pack: &mut ContainerWriter,
220 name: &str,
221 pairs: ExternalSorter<CellRef>,
222) -> Result<u64> {
223 let mut blocks: Vec<(u64, u64)> = Vec::new();
224 let mut cell_count = 0u64;
225 let mut data_len = 0u64;
226 let mut previous_cell = 0u64;
227 let mut current: Option<u64> = None;
228 let mut points = Vec::new();
229 let mut segments = Vec::new();
230 let mut entry = Vec::new();
231 let mut refs = Vec::new();
232
233 pack.begin(&format!("{name}/data"), SPATIAL_VERSION)?;
234 let mut flush = |pack: &mut ContainerWriter,
235 cell: u64,
236 points: &mut Vec<u64>,
237 segments: &mut Vec<u64>|
238 -> Result<()> {
239 if cell_count % CELLS_PER_BLOCK == 0 {
240 blocks.push((cell, data_len));
241 previous_cell = cell;
242 }
243 refs.clear();
244 put_delta_list(&mut refs, points);
245 put_delta_list(&mut refs, segments);
246 entry.clear();
247 put_u64(&mut entry, cell - previous_cell);
248 put_u64(&mut entry, refs.len() as u64);
249 pack.write_all(&entry)?;
250 pack.write_all(&refs)?;
251 data_len += (entry.len() + refs.len()) as u64;
252 previous_cell = cell;
253 cell_count += 1;
254 points.clear();
255 segments.clear();
256 Ok(())
257 };
258 let mut last: Option<CellRef> = None;
259 for pair in pairs.finish()? {
260 let pair = pair?;
261 if last == Some(pair) {
262 continue;
263 }
264 last = Some(pair);
265 if current.is_some_and(|cell| cell != pair.cell) {
266 flush(
267 pack,
268 current.expect("current cell"),
269 &mut points,
270 &mut segments,
271 )?;
272 }
273 current = Some(pair.cell);
274 match pair.kind {
275 KIND_POINT => points.push(pair.reference),
276 _ => segments.push(pair.reference),
277 }
278 }
279 if let Some(cell) = current {
280 flush(pack, cell, &mut points, &mut segments)?;
281 }
282 pack.end()?;
283
284 let mut index = Vec::with_capacity(24 + blocks.len() * 16);
285 index.extend_from_slice(&cell_count.to_le_bytes());
286 index.extend_from_slice(&(blocks.len() as u64).to_le_bytes());
287 for (first_cell, offset) in &blocks {
288 index.extend_from_slice(&first_cell.to_le_bytes());
289 index.extend_from_slice(&offset.to_le_bytes());
290 }
291 index.extend_from_slice(&data_len.to_le_bytes());
292 pack.add(&format!("{name}/index"), SPATIAL_VERSION, &index)?;
293 Ok(cell_count)
294}
295
296fn put_delta_list(out: &mut Vec<u8>, values: &[u64]) {
297 put_u64(out, values.len() as u64);
298 let mut previous = 0;
299 for value in values {
300 put_u64(out, value - previous);
301 previous = *value;
302 }
303}
304
305fn get_delta_list(input: &mut &[u8]) -> Result<Vec<u64>> {
306 let count = usize::try_from(get_u64(input)?)?;
307 if count > input.len() {
308 bail!("spatial reference list is truncated");
309 }
310 let mut values = Vec::with_capacity(count);
311 let mut previous = 0u64;
312 for _ in 0..count {
313 previous = previous
314 .checked_add(get_u64(input)?)
315 .context("spatial reference overflows")?;
316 values.push(previous);
317 }
318 Ok(values)
319}
320
321#[derive(Clone)]
322struct CellTable {
323 index: Bytes,
324 data: Bytes,
325 block_count: u64,
326}
327
328struct CellRefs {
329 points: Vec<u64>,
330 segments: Vec<u64>,
331}
332
333impl CellTable {
334 fn open(container: &Container, name: &str) -> Result<Self> {
335 let index = container.section(&format!("{name}/index"), SPATIAL_VERSION)?;
336 let data = container.section(&format!("{name}/data"), SPATIAL_VERSION)?;
337 let cell_count = read_u64_le(&index, 0).context("spatial index is truncated")?;
338 let block_count = read_u64_le(&index, 8).context("spatial index is truncated")?;
339 if block_count != cell_count.div_ceil(CELLS_PER_BLOCK)
340 || index.len() as u64 != 24 + block_count * 16
341 || read_u64_le(&index, index.len() - 8) != Some(data.len() as u64)
342 {
343 bail!("spatial index {name} is inconsistent with its data");
344 }
345 Ok(Self {
346 index,
347 data,
348 block_count,
349 })
350 }
351
352 fn block(&self, block: u64) -> (u64, u64) {
353 let offset = 16 + block as usize * 16;
354 (
355 read_u64_le(&self.index, offset).expect("validated index"),
356 read_u64_le(&self.index, offset + 8).expect("validated index"),
357 )
358 }
359
360 fn refs(&self, cell: u64) -> Result<Option<CellRefs>> {
361 let (mut low, mut high) = (0, self.block_count);
363 while low < high {
364 let mid = low + (high - low) / 2;
365 if self.block(mid).0 <= cell {
366 low = mid + 1;
367 } else {
368 high = mid;
369 }
370 }
371 let Some(block) = low.checked_sub(1) else {
372 return Ok(None);
373 };
374 let (first_cell, start) = self.block(block);
375 let end = if block + 1 < self.block_count {
376 self.block(block + 1).1
377 } else {
378 self.data.len() as u64
379 };
380 let mut input = self
381 .data
382 .get(start as usize..end as usize)
383 .context("spatial block is out of range")?;
384 let mut current = first_cell;
385 while !input.is_empty() {
386 current += get_u64(&mut input)?;
387 let len = usize::try_from(get_u64(&mut input)?)?;
388 let mut refs = input.get(..len).context("spatial cell is truncated")?;
389 input = &input[len..];
390 match current.cmp(&cell) {
391 Ordering::Less => continue,
392 Ordering::Greater => return Ok(None),
393 Ordering::Equal => {
394 return Ok(Some(CellRefs {
395 points: get_delta_list(&mut refs)?,
396 segments: get_delta_list(&mut refs)?,
397 }));
398 }
399 }
400 }
401 Ok(None)
402 }
403}
404
405#[derive(Clone)]
406pub struct SpatialIndexReader {
407 fine: CellTable,
408 context: CellTable,
409 records: RecordsReader,
410}
411
412impl std::fmt::Debug for SpatialIndexReader {
413 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 formatter
415 .debug_struct("SpatialIndexReader")
416 .field("fine_blocks", &self.fine.block_count)
417 .field("context_blocks", &self.context.block_count)
418 .finish()
419 }
420}
421
422struct LineCache {
423 points: Vec<[f64; 2]>,
424 lengths: Vec<f64>,
426}
427
428impl SpatialIndexReader {
429 pub fn open(container: &Container, records: RecordsReader) -> Result<Self> {
430 Ok(Self {
431 fine: CellTable::open(container, FINE)?,
432 context: CellTable::open(container, CONTEXT)?,
433 records,
434 })
435 }
436
437 pub fn point_candidates(
438 &self,
439 lon: f64,
440 lat: f64,
441 layer: Layer,
442 radius_m: f64,
443 limit: usize,
444 ) -> Result<Vec<PointCandidate>> {
445 let candidates = self.collect_points(
446 &self.fine,
447 H3_FINE_RESOLUTION,
448 lon,
449 lat,
450 radius_m,
451 limit,
452 |candidate| candidate == layer,
453 )?;
454 Ok(closest_candidates(candidates, limit))
455 }
456
457 pub fn context_candidates(
458 &self,
459 lon: f64,
460 lat: f64,
461 radius_m: f64,
462 limit: usize,
463 ) -> Result<Vec<PointCandidate>> {
464 let candidates = self.collect_points(
465 &self.context,
466 H3_CONTEXT_RESOLUTION,
467 lon,
468 lat,
469 radius_m,
470 limit,
471 Layer::is_context,
472 )?;
473 Ok(closest_candidates(candidates, limit))
474 }
475
476 pub fn any_context_point(
480 &self,
481 lon: f64,
482 lat: f64,
483 layer: Layer,
484 radius_m: f64,
485 mut matches: impl FnMut(RecordId) -> Result<bool>,
486 ) -> Result<Option<bool>> {
487 let Ok(lat_lng) = LatLng::new(lat, lon) else {
488 return Ok(None);
489 };
490 let mut cells = lat_lng
491 .to_cell(H3_CONTEXT_RESOLUTION)
492 .grid_disk_distances::<Vec<(CellIndex, u32)>>(disk_k(H3_CONTEXT_RESOLUTION, radius_m));
493 cells.sort_unstable_by_key(|(cell, k)| (*k, u64::from(*cell)));
494 let mut seen = false;
495 for (cell, _) in cells {
496 let Some(refs) = self.context.refs(u64::from(cell))? else {
497 continue;
498 };
499 for record_id in refs.points {
500 let (point_layer, point_lon, point_lat) = self.records.point(record_id)?;
501 if point_layer != layer || haversine_m(lon, lat, point_lon, point_lat) > radius_m {
502 continue;
503 }
504 seen = true;
505 if matches(record_id)? {
506 return Ok(Some(true));
507 }
508 }
509 }
510 Ok(seen.then_some(false))
511 }
512
513 pub fn segment_candidates(
514 &self,
515 lon: f64,
516 lat: f64,
517 layer: Layer,
518 radius_m: f64,
519 limit: usize,
520 ) -> Result<Vec<SegmentCandidate>> {
521 let mut seen = std::collections::BTreeSet::new();
522 let mut lines: HashMap<RecordId, Option<LineCache>> = HashMap::new();
523 let mut candidates = Vec::new();
524 for cell in query_cells(lon, lat, H3_FINE_RESOLUTION, radius_m) {
525 let Some(refs) = self.fine.refs(cell)? else {
526 continue;
527 };
528 for reference in refs.segments {
529 if !seen.insert(reference) {
530 continue;
531 }
532 let record_id = reference >> SEGMENT_INDEX_BITS;
533 let segment = (reference & ((1 << SEGMENT_INDEX_BITS) - 1)) as usize;
534 let line = match lines.entry(record_id) {
535 std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
536 std::collections::hash_map::Entry::Vacant(entry) => {
537 entry.insert(if self.records.header(record_id)?.layer == layer {
538 self.records.line(record_id)?.map(line_cache)
539 } else {
540 None
541 })
542 }
543 };
544 let Some(line) = line.as_ref() else {
545 continue;
546 };
547 let (Some(start), Some(end)) =
548 (line.points.get(segment), line.points.get(segment + 1))
549 else {
550 bail!("segment reference {reference} is out of range");
551 };
552 let projection = project_to_segment_m(lon, lat, *start, *end);
553 if projection.distance_m <= radius_m {
554 let total = *line.lengths.last().expect("line has vertices");
555 let start_fraction = line.lengths[segment] / total;
556 let end_fraction = line.lengths[segment + 1] / total;
557 candidates.push(SegmentCandidate {
558 record_id,
559 layer,
560 closest_lon: projection.lon,
561 closest_lat: projection.lat,
562 distance_m: projection.distance_m,
563 fraction: start_fraction + projection.t * (end_fraction - start_fraction),
564 });
565 }
566 }
567 }
568 Ok(closest_candidates(candidates, limit))
569 }
570
571 #[allow(clippy::too_many_arguments)]
578 fn collect_points(
579 &self,
580 index: &CellTable,
581 resolution: Resolution,
582 lon: f64,
583 lat: f64,
584 radius_m: f64,
585 limit: usize,
586 accepts: impl Fn(Layer) -> bool,
587 ) -> Result<Vec<PointCandidate>> {
588 let Ok(lat_lng) = LatLng::new(lat, lon) else {
589 return Ok(Vec::new());
590 };
591 let max_k = disk_k(resolution, radius_m);
592 let mut cells = lat_lng
593 .to_cell(resolution)
594 .grid_disk_distances::<Vec<(CellIndex, u32)>>(max_k);
595 cells.sort_unstable_by_key(|(cell, k)| (*k, u64::from(*cell)));
596
597 let mut candidates = Vec::new();
598 let mut next = 0;
599 for k in 0..=max_k {
600 while let Some(&(cell, ring)) = cells.get(next)
601 && ring == k
602 {
603 next += 1;
604 let Some(refs) = index.refs(u64::from(cell))? else {
605 continue;
606 };
607 for record_id in refs.points {
608 let (layer, point_lon, point_lat) = self.records.point(record_id)?;
609 if !accepts(layer) {
610 continue;
611 }
612 let distance_m = haversine_m(lon, lat, point_lon, point_lat);
613 if distance_m <= radius_m {
614 candidates.push(PointCandidate {
615 record_id,
616 layer,
617 lon: point_lon,
618 lat: point_lat,
619 distance_m,
620 });
621 }
622 }
623 }
624 if limit > 0 && candidates.len() >= limit && k < max_k {
625 let covered_m =
628 f64::from(k.saturating_sub(H3_RADIUS_EXTRA_RING)) * resolution.edge_length_m();
629 let mut distances = candidates
630 .iter()
631 .map(|candidate| candidate.distance_m)
632 .collect::<Vec<_>>();
633 let (_, kth, _) = distances.select_nth_unstable_by(limit - 1, f64::total_cmp);
634 if *kth < covered_m {
635 break;
636 }
637 }
638 }
639 Ok(candidates)
640 }
641}
642
643fn line_cache(points: Vec<[i32; 2]>) -> LineCache {
644 let points = points
645 .into_iter()
646 .map(|[lon, lat]| [dequantize(lon), dequantize(lat)])
647 .collect::<Vec<_>>();
648 let mut lengths = Vec::with_capacity(points.len());
649 let mut total = 0.0;
650 lengths.push(0.0);
651 for pair in points.windows(2) {
652 total += haversine_m(pair[0][0], pair[0][1], pair[1][0], pair[1][1]);
653 lengths.push(total);
654 }
655 LineCache { points, lengths }
656}
657
658fn cell_id(lon: f64, lat: f64, resolution: Resolution) -> Result<u64> {
659 let lat_lng = LatLng::new(lat, lon).context("invalid coordinate for h3 cell")?;
660 Ok(u64::from(lat_lng.to_cell(resolution)))
661}
662
663fn disk_k(resolution: Resolution, radius_m: f64) -> u32 {
665 if radius_m <= 0.0 {
666 H3_RADIUS_EXTRA_RING
667 } else {
668 ((radius_m / resolution.edge_length_m()).ceil() as u32)
669 .saturating_add(H3_RADIUS_EXTRA_RING)
670 .min(H3_MAX_QUERY_K)
671 }
672}
673
674fn query_cells(lon: f64, lat: f64, resolution: Resolution, radius_m: f64) -> Vec<u64> {
675 let Ok(lat_lng) = LatLng::new(lat, lon) else {
676 return Vec::new();
677 };
678 let mut cells = lat_lng
679 .to_cell(resolution)
680 .grid_disk::<Vec<CellIndex>>(disk_k(resolution, radius_m))
681 .into_iter()
682 .map(u64::from)
683 .collect::<Vec<_>>();
684 cells.sort_unstable();
686 cells
687}
688
689fn segment_cells(start: [f64; 2], end: [f64; 2], resolution: Resolution) -> Result<Vec<u64>> {
690 let length = haversine_m(start[0], start[1], end[0], end[1]);
691 let step = (resolution.edge_length_m() / H3_SEGMENT_SAMPLE_DIVISOR).max(1.0);
692 let sample_count = ((length / step).ceil() as usize).max(1);
693 let mut cells = Vec::with_capacity(sample_count + 1);
694 for sample in 0..=sample_count {
695 let t = sample as f64 / sample_count as f64;
696 let lon = start[0] + t * (end[0] - start[0]);
697 let lat = start[1] + t * (end[1] - start[1]);
698 cells.push(cell_id(lon, lat, resolution)?);
699 }
700 cells.sort_unstable();
701 cells.dedup();
702 Ok(cells)
703}
704
705struct SegmentProjection {
706 lon: f64,
707 lat: f64,
708 distance_m: f64,
709 t: f64,
710}
711
712fn project_to_segment_m(lon: f64, lat: f64, start: [f64; 2], end: [f64; 2]) -> SegmentProjection {
713 let (sx, sy) = local_xy_m(start[0], start[1], lon, lat);
714 let (ex, ey) = local_xy_m(end[0], end[1], lon, lat);
715 let vx = ex - sx;
716 let vy = ey - sy;
717 let length_2 = vx * vx + vy * vy;
718 let t = if length_2 <= f64::EPSILON {
719 0.0
720 } else {
721 (-(sx * vx + sy * vy) / length_2).clamp(0.0, 1.0)
722 };
723 let x = sx + t * vx;
724 let y = sy + t * vy;
725 SegmentProjection {
726 lon: start[0] + t * (end[0] - start[0]),
727 lat: start[1] + t * (end[1] - start[1]),
728 distance_m: (x * x + y * y).sqrt(),
729 t,
730 }
731}
732
733fn local_xy_m(lon: f64, lat: f64, origin_lon: f64, origin_lat: f64) -> (f64, f64) {
734 let x = (lon - origin_lon).to_radians() * EARTH_RADIUS_M * origin_lat.to_radians().cos();
735 let y = (lat - origin_lat).to_radians() * EARTH_RADIUS_M;
736 (x, y)
737}
738
739pub(crate) fn haversine_m(a_lon: f64, a_lat: f64, b_lon: f64, b_lat: f64) -> f64 {
740 let d_lat = (b_lat - a_lat).to_radians();
741 let d_lon = (b_lon - a_lon).to_radians();
742 let a_lat = a_lat.to_radians();
743 let b_lat = b_lat.to_radians();
744 let sin_d_lat = (d_lat / 2.0).sin();
745 let sin_d_lon = (d_lon / 2.0).sin();
746 let h = sin_d_lat * sin_d_lat + a_lat.cos() * b_lat.cos() * sin_d_lon * sin_d_lon;
747 2.0 * EARTH_RADIUS_M * h.sqrt().asin()
748}
749
750trait CandidateDistance {
751 fn distance_m(&self) -> f64;
752}
753
754impl CandidateDistance for PointCandidate {
755 fn distance_m(&self) -> f64 {
756 self.distance_m
757 }
758}
759
760impl CandidateDistance for SegmentCandidate {
761 fn distance_m(&self) -> f64 {
762 self.distance_m
763 }
764}
765
766fn compare_distance<T: CandidateDistance>(left: &T, right: &T) -> Ordering {
767 left.distance_m()
768 .partial_cmp(&right.distance_m())
769 .unwrap_or(Ordering::Equal)
770}
771
772fn closest_candidates<T: CandidateDistance>(mut candidates: Vec<T>, limit: usize) -> Vec<T> {
773 if limit > 0 && candidates.len() > limit {
774 let mut indices: Vec<usize> = (0..candidates.len()).collect();
777 indices.select_nth_unstable_by(limit - 1, |&left, &right| {
778 compare_distance(&candidates[left], &candidates[right]).then_with(|| left.cmp(&right))
779 });
780 let cutoff_index = indices[limit - 1];
781 let cutoff_distance = candidates[cutoff_index].distance_m();
782 let mut index = 0;
783 candidates.retain(|candidate| {
784 let keep = candidate
785 .distance_m()
786 .partial_cmp(&cutoff_distance)
787 .unwrap_or(Ordering::Equal)
788 .then_with(|| index.cmp(&cutoff_index))
789 != Ordering::Greater;
790 index += 1;
791 keep
792 });
793 }
794 candidates.sort_by(compare_distance);
795 candidates
796}
797
798#[cfg(test)]
799mod tests {
800 use geojson::Geometry;
801
802 use super::*;
803 use crate::{
804 record::{
805 AddressComponents, AddressRecord, DerivedSourceProvenance, LocationPrecision,
806 OsmObjectType, PostcodeRecord, SourceProvenance, StreetRecord, point_geometry,
807 },
808 records::RecordsWriter,
809 };
810
811 #[test]
812 fn closest_candidates_matches_stable_full_sort() {
813 for count in [0, 1, 8, 1_000] {
814 let candidates: Vec<_> = (0..count)
815 .map(|id| PointCandidate {
816 record_id: id,
817 layer: Layer::Address,
818 lon: 0.0,
819 lat: 0.0,
820 distance_m: ((id * 37) % 23) as f64,
822 })
823 .collect();
824 for limit in [0, 1, 3, 5, 8, 999, 1_000, 1_001] {
825 let mut expected = candidates.clone();
826 expected.sort_by(compare_distance);
827 if limit > 0 {
828 expected.truncate(limit);
829 }
830 let actual = closest_candidates(candidates.clone(), limit);
831 assert_eq!(
832 actual.iter().map(|hit| hit.record_id).collect::<Vec<_>>(),
833 expected.iter().map(|hit| hit.record_id).collect::<Vec<_>>(),
834 "count={count}, limit={limit}",
835 );
836 }
837 }
838 }
839
840 fn address(lon: f64, lat: f64, object_id: i64) -> Record {
841 Record::Address(AddressRecord {
842 address: AddressComponents {
843 number: "10".to_string(),
844 street: Some("King Street".to_string()),
845 place: None,
846 unit: None,
847 locality: None,
848 region: None,
849 postcode: None,
850 country: None,
851 },
852 geometry: point_geometry(lon, lat),
853 location_precision: LocationPrecision::Point,
854 source: SourceProvenance::osm(OsmObjectType::Node, object_id),
855 })
856 }
857
858 fn build(records: &[Record]) -> (SpatialIndexReader, SpatialCommit) {
861 let root =
862 std::env::temp_dir().join(format!("open-geocode-spatial-{}", uuid::Uuid::new_v4()));
863 let scratch = Scratch::create(root.join("scratch")).expect("scratch");
864 let mut writer =
865 RecordsWriter::create(&scratch, &MemoryBudget::unlimited()).expect("records");
866 let mut spatial = SpatialIndexWriter::new(&scratch, &MemoryBudget::unlimited(), 64);
867 for record in records {
868 let id = writer.write(record, None).expect("write");
869 spatial
870 .add(RecordCells::for_record(id, record).expect("cells"))
871 .expect("add");
872 }
873 let path = root.join("pack.ogp");
874 let mut pack = ContainerWriter::create(&path).expect("pack");
875 writer.finish(&mut pack).expect("records finish");
876 let commit = spatial.finish(&mut pack).expect("spatial finish");
877 pack.finish().expect("pack finish");
878 let container = Container::open(&path).expect("open");
879 let records = RecordsReader::open(&container).expect("records reader");
880 (
881 SpatialIndexReader::open(&container, records).expect("spatial reader"),
882 commit,
883 )
884 }
885
886 #[test]
887 fn indexes_and_queries_address_points() {
888 let mut records = vec![address(-79.0, 43.0, 7)];
889 for id in 8..1_008 {
890 records.push(address(
891 -79.0,
892 43.0 + ((id * 37) % 100) as f64 * 0.000001,
893 id,
894 ));
895 }
896 let (reader, commit) = build(&records);
897 assert!(commit.fine_pairs.runs > 0, "the tiny budget must spill");
898 assert_eq!(commit.point_count, 1_001);
899
900 let hits = reader
901 .point_candidates(-79.0, 43.0, Layer::Address, 5.0, 1)
902 .expect("hits");
903 assert_eq!(hits.len(), 1);
904 assert_eq!(hits[0].record_id, 0);
905
906 let all = reader
907 .point_candidates(-79.0, 43.0, Layer::Address, 100.0, 0)
908 .expect("all");
909 let limited = reader
910 .point_candidates(-79.0, 43.0, Layer::Address, 100.0, 5)
911 .expect("limited");
912 assert_eq!(all.len(), 1_001);
913 assert_eq!(
914 limited.iter().map(|hit| hit.record_id).collect::<Vec<_>>(),
915 all.iter()
916 .take(5)
917 .map(|hit| hit.record_id)
918 .collect::<Vec<_>>(),
919 );
920 assert!(
921 reader
922 .point_candidates(-79.0, 43.0, Layer::Postcode, 100.0, 0)
923 .expect("other layer")
924 .is_empty()
925 );
926 }
927
928 #[test]
929 fn early_stopping_ring_scan_matches_the_full_scan() {
930 let mut state = 0x2545_f491_4f6c_dd1du64;
932 let mut next = || {
933 state ^= state << 13;
934 state ^= state >> 7;
935 state ^= state << 17;
936 (state % 1_000_000) as f64 / 1_000_000.0 - 0.5
937 };
938 let records = (0..2_000)
939 .map(|index| {
940 let spread = if index % 3 == 0 { 0.6 } else { 0.05 };
941 Record::Postcode(PostcodeRecord {
942 postcode: format!("P{index}"),
943 geometry: point_geometry(-79.4 + next() * spread, 43.7 + next() * spread),
944 source: DerivedSourceProvenance::osm_address_records(1),
945 })
946 })
947 .collect::<Vec<_>>();
948 let (reader, _) = build(&records);
949 for (lon, lat) in [(-79.4, 43.7), (-79.6, 43.9), (-79.1, 43.5), (-78.0, 43.0)] {
950 for limit in [1, 16, 100] {
951 let early = reader
952 .context_candidates(lon, lat, 50_000.0, limit)
953 .expect("early");
954 let mut full = reader
955 .context_candidates(lon, lat, 50_000.0, 0)
956 .expect("full");
957 full.truncate(limit);
958 let distances = |hits: &[PointCandidate]| {
959 hits.iter().map(|hit| hit.distance_m).collect::<Vec<_>>()
960 };
961 assert_eq!(
962 distances(&early),
963 distances(&full),
964 "{lon},{lat} limit {limit}"
965 );
966 }
967 }
968 }
969
970 #[test]
971 fn indexes_context_points_with_coarse_cells() {
972 let (reader, _) = build(&[Record::Postcode(PostcodeRecord {
973 postcode: "M5V".to_string(),
974 geometry: point_geometry(-79.4, 43.6),
975 source: DerivedSourceProvenance::osm_address_records(1),
976 })]);
977 let hits = reader
978 .context_candidates(-79.39, 43.6, 5_000.0, 5)
979 .expect("hits");
980 assert_eq!(hits.len(), 1);
981 assert_eq!(hits[0].record_id, 0);
982 assert_eq!(hits[0].layer, Layer::Postcode);
983 }
984
985 #[test]
986 fn indexes_line_segments_with_fraction() {
987 let street = |object_id: i64, lon: f64| {
988 Record::Street(StreetRecord {
989 name: "King Street".to_string(),
990 geometry: Geometry::new(GeometryValue::LineString {
991 coordinates: vec![
992 vec![lon, 43.0].into(),
993 vec![lon, 43.0005].into(),
994 vec![lon, 43.001].into(),
995 ],
996 }),
997 representative_point: [lon, 43.0005],
998 source: SourceProvenance::osm(OsmObjectType::Way, object_id),
999 })
1000 };
1001 let (reader, commit) = build(&[street(1, -79.0), street(2, -78.99)]);
1002 assert_eq!(commit.segment_count, 4);
1003 let hits = reader
1004 .segment_candidates(-79.00001, 43.00075, Layer::Street, 5.0, 1)
1005 .expect("hits");
1006 assert_eq!(hits.len(), 1);
1007 assert_eq!(hits[0].record_id, 0);
1008 assert!(
1009 (hits[0].fraction - 0.75).abs() < 0.01,
1010 "{}",
1011 hits[0].fraction
1012 );
1013 assert!(
1014 reader
1015 .segment_candidates(-79.00001, 43.00075, Layer::Interpolation, 5.0, 1)
1016 .expect("other layer")
1017 .is_empty()
1018 );
1019 }
1020}