1mod address;
18mod boundary;
19mod emit;
20mod emitted;
21mod geometry;
22mod interpolation;
23mod nodes;
24mod pbf;
25mod place;
26mod postcode;
27mod scan;
28mod spill;
29mod street;
30mod tags;
31
32#[cfg(test)]
33mod test_pbf;
34
35use std::{
36 collections::{HashMap, HashSet},
37 fs::{self, File},
38 io::{BufWriter, Write},
39 path::{Path, PathBuf},
40 sync::Arc,
41 time::Instant,
42};
43
44use anyhow::{Context, Result, bail};
45use rayon::prelude::*;
46
47use crate::{
48 builder::{
49 progress::{item_progress_bar, stage_progress},
50 report::{BUILD_REPORT_SCHEMA_VERSION, BuilderReport, SorterReport},
51 },
52 extsort::{ExternalSorter, SortStats, Spool},
53 memory::MemoryBudget,
54 pack::{
55 AUDIT_DIR, BUILD_REPORT_FILE, PackWriter, PackWriterOptions, REJECTIONS_FILE, RecordContext,
56 },
57 record::{Record, RejectedRecord},
58 records::quantize,
59 util::hilbert::hilbert_key,
60};
61use boundary::{BoundaryIndex, ContextOrigin, Vertex, build_boundaries, record_context};
62use emit::{FeatureOutput, emit_features};
63use emitted::Emitted;
64use nodes::join_nodes;
65use pbf::{for_each_block, input_bytes};
66use postcode::PostcodeAccumulator;
67use scan::scan_block;
68use spill::{NodeRequest, PendingRecord, ResolvedRef, WayFeature, WayKind};
69
70pub const DEFAULT_MEMORY_BUDGET_BYTES: usize = 1 << 30;
72const WRITE_BATCH_RECORDS: usize = 16_384;
74
75#[derive(Debug, Clone)]
76pub struct BuildOsmOptions {
77 pub input: PathBuf,
78 pub pack: PathBuf,
79 pub memory_budget_bytes: usize,
81 pub scratch_dir: Option<PathBuf>,
83}
84
85impl Default for BuildOsmOptions {
86 fn default() -> Self {
87 Self {
88 input: PathBuf::new(),
89 pack: PathBuf::new(),
90 memory_budget_bytes: DEFAULT_MEMORY_BUDGET_BYTES,
91 scratch_dir: None,
92 }
93 }
94}
95
96pub fn build_osm_pack(options: BuildOsmOptions) -> Result<BuilderReport> {
97 let started = Instant::now();
98 if !options.input.is_file() {
99 bail!("input {} does not exist", options.input.display());
100 }
101 let sorter_budget = (options.memory_budget_bytes / 4).max(1);
105 let memory = MemoryBudget::new(options.memory_budget_bytes);
106 let mut writer = PackWriter::create_with(
107 &options.pack,
108 PackWriterOptions {
109 scratch_dir: options.scratch_dir.clone(),
110 memory: Arc::clone(&memory),
111 sort_budget_bytes: sorter_budget,
112 text_index_memory_bytes: sorter_budget,
113 },
114 )?;
115 let scratch = Arc::clone(writer.scratch());
116 let mut build = Build {
117 report: BuilderReport {
118 schema_version: BUILD_REPORT_SCHEMA_VERSION,
119 input: options.input.display().to_string(),
120 pack: options.pack.display().to_string(),
121 input_bytes: input_bytes(&options.input)?,
122 ..BuilderReport::default()
123 },
124 audit: Audit::create(writer.generation())?,
125 postcodes: PostcodeAccumulator::default(),
126 records: ExternalSorter::new(&scratch, "records", &memory, sorter_budget),
127 context_records: ExternalSorter::new(
128 &scratch,
129 "context-records",
130 &memory,
131 sorter_budget / 4,
132 ),
133 seq: 0,
134 };
135 build.report.scratch.memory_budget_bytes = options.memory_budget_bytes as u64;
136
137 let phase = Instant::now();
139 let mut requests = ExternalSorter::new(&scratch, "node-requests", &memory, sorter_budget);
140 let mut features = Spool::<WayFeature>::create(&scratch, "way-features")?;
141 let mut relations = Vec::new();
142 let mut boundary_feature_ids = HashSet::new();
145 let mut node_blobs = Vec::new();
146 let mut way_blobs = Vec::new();
147 for_each_block(
148 &options.input,
149 "1/7 scan OSM objects",
150 None,
151 |offset, block| Ok((offset, scan_block(&block))),
152 |(offset, output)| {
153 if output.has_nodes {
154 node_blobs.push(offset);
155 }
156 if output.has_ways {
157 way_blobs.push(offset);
158 }
159 for (feature, refs) in output.ways {
160 let resolution = &mut build.report.geometry_resolution;
161 match feature.kind {
162 WayKind::Address => resolution.address_way_stubs += 1,
163 WayKind::Street => resolution.street_way_stubs += 1,
164 WayKind::Interpolation => resolution.interpolation_way_stubs += 1,
165 WayKind::Boundary => {
166 resolution.boundary_way_stubs += 1;
167 boundary_feature_ids.insert(feature.way_id);
168 }
169 }
170 request_nodes(
171 &mut requests,
172 features.written(),
173 &refs,
174 feature.kind == WayKind::Interpolation,
175 )?;
176 features.write(&feature)?;
177 }
178 relations.extend(output.relations);
179 build.absorb(output.emitted)
180 },
181 )?;
182 build.report.phases.scan_ms = phase.elapsed().as_millis();
183
184 let phase = Instant::now();
187 let member_base = features.written();
188 let member_ids = boundary::required_boundary_way_ids(&relations)
189 .into_iter()
190 .filter(|way_id| !boundary_feature_ids.contains(way_id))
191 .collect::<HashSet<_>>();
192 let mut members: HashMap<i64, (u64, usize)> = HashMap::new();
193 if !member_ids.is_empty() {
194 for_each_block(
195 &options.input,
196 "2/7 read boundary member ways",
197 Some(&way_blobs),
198 |_, block| Ok(member_ways(&block, &member_ids)),
199 |found| {
200 for (way_id, refs) in found {
201 if members.contains_key(&way_id) {
202 continue;
203 }
204 let owner = member_base + members.len() as u64;
205 request_nodes(&mut requests, owner, &refs, false)?;
206 members.insert(way_id, (owner, refs.len()));
207 }
208 Ok(())
209 },
210 )?;
211 }
212 build.report.phases.boundary_member_scan_ms = phase.elapsed().as_millis();
213
214 let phase = Instant::now();
216 let request_stats = requests.stats();
217 build.report.geometry_resolution.required_node_refs = request_stats.items;
218 let mut resolved =
219 ExternalSorter::<ResolvedRef>::new(&scratch, "resolved-nodes", &memory, sorter_budget);
220 let join = join_nodes(
221 &options.input,
222 &node_blobs,
223 requests.finish()?,
224 &mut resolved,
225 )?;
226 build.report.geometry_resolution.resolved_node_refs = join.resolved;
227 build.report.phases.node_join_ms = phase.elapsed().as_millis();
228
229 let phase = Instant::now();
231 let resolved_stats = resolved.stats();
232 let mut boundary_ways = Vec::new();
233 let progress = item_progress_bar(features.written(), "4/7 assemble ways");
234 let mut member_vertices = emit_features(
235 features.into_reader()?,
236 resolved.finish()?,
237 |output: FeatureOutput| {
238 progress.inc(1);
239 boundary_ways.extend(output.boundary_ways);
240 build.absorb(output.emitted)
241 },
242 )?;
243 progress.finish_with_message("4/7 assemble ways complete");
244 build.report.phases.feature_emission_ms = phase.elapsed().as_millis();
245
246 let phase = Instant::now();
248 let member_vertices: HashMap<i64, Option<Vec<Vertex>>> = members
249 .into_iter()
250 .map(|(way_id, (owner, count))| {
251 let line = member_vertices
252 .remove(&owner)
253 .filter(|vertices| vertices.len() == count);
254 (way_id, line)
255 })
256 .collect();
257 let mut member_lines: HashMap<i64, Option<&[Vertex]>> = boundary_feature_ids
260 .iter()
261 .map(|way_id| (*way_id, None))
262 .collect();
263 for way in &boundary_ways {
264 member_lines.insert(way.object_id, Some(way.vertices.as_slice()));
265 }
266 for (way_id, line) in &member_vertices {
267 member_lines.insert(*way_id, line.as_deref());
268 }
269 let boundaries = build_boundaries(&boundary_ways, &relations, &member_lines);
270 drop(member_lines);
271 drop((
272 boundary_ways,
273 member_vertices,
274 relations,
275 boundary_feature_ids,
276 ));
277 for (origin, record) in boundaries.place_records(&mut build.report) {
278 build.push(record, Some(origin))?;
279 }
280 for record in std::mem::take(&mut build.postcodes).into_records() {
281 build.report.accept_postcode();
282 build.push(Record::Postcode(record), None)?;
283 }
284 build.report.phases.boundary_build_ms = phase.elapsed().as_millis();
285
286 let phase = Instant::now();
289 let Build {
290 mut report,
291 audit,
292 records,
293 context_records,
294 ..
295 } = build;
296 let context_stats = context_records.stats();
297 let record_stats = records.stats();
298 let mut ordered = Spool::<PendingRecord>::create(&scratch, "context-ordered")?;
299 let mut boundary_ids = vec![0; boundaries.len()];
300 let mut country_ids = HashMap::new();
301 let first_id = writer.record_count();
302 for (index, item) in context_records.finish()?.enumerate() {
303 let item = item?;
304 let record_id = first_id + index as u64;
305 match &item.origin {
306 Some(ContextOrigin::Boundary(boundary)) => boundary_ids[*boundary as usize] = record_id,
307 Some(ContextOrigin::DerivedCountry(code)) => {
308 country_ids.insert(code.clone(), record_id);
309 }
310 None => {}
311 }
312 ordered.write(&item)?;
313 }
314 let index = boundaries.into_index(&boundary_ids, &country_ids);
315 let context_count = ordered.written();
316 write_ordered(
317 &mut writer,
318 ordered.into_reader()?,
319 context_count,
320 &index,
321 "5/7 write context records",
322 )?;
323 report.phases.context_record_write_ms = phase.elapsed().as_millis();
324
325 let phase = Instant::now();
326 write_ordered(
327 &mut writer,
328 records.finish()?,
329 record_stats.items,
330 &index,
331 "6/7 write records",
332 )?;
333 drop(index);
334 report.phases.record_write_ms = phase.elapsed().as_millis();
335
336 let phase = Instant::now();
337 report.scratch.spilled_bytes = scratch.spilled_bytes();
338 report.scratch.peak_tracked_bytes = memory.peak() as u64;
339 drop(scratch);
340 let progress = stage_progress("7/7 finish Pack indexes");
341 let (sealed, stats) = writer.seal()?;
342 progress.finish_with_message("7/7 finish Pack indexes complete");
343 report.phases.pack_seal_ms = phase.elapsed().as_millis();
344
345 for (name, stats) in [
346 ("node_requests", request_stats),
347 ("resolved_nodes", resolved_stats),
348 ("records", record_stats),
349 ("context_records", context_stats),
350 (
351 "spatial_pairs",
352 SortStats {
353 items: 0,
354 runs: stats.spatial_pair_runs,
355 },
356 ),
357 ] {
358 report.scratch.sorters.insert(
359 name.to_string(),
360 SorterReport {
361 items: stats.items,
362 runs: stats.runs,
363 },
364 );
365 }
366 report.output.pack_bytes = stats.bytes;
367 report.output.record_count = sealed.manifest().record_count;
368 report.output.text_index_bytes = stats.text_index_bytes;
369 report.output.sections = stats.sections;
370 report.output.rejection_count = audit.count;
371 report.phases.total_ms = started.elapsed().as_millis();
372 report.finalize_throughput();
373 audit.finish(&report)?;
374 sealed.publish()?;
375 Ok(report)
376}
377
378struct Build {
380 report: BuilderReport,
381 audit: Audit,
382 postcodes: PostcodeAccumulator,
383 records: ExternalSorter<PendingRecord>,
385 context_records: ExternalSorter<PendingRecord>,
387 seq: u64,
388}
389
390impl Build {
391 fn absorb(&mut self, emitted: Emitted) -> Result<()> {
393 let Emitted {
394 report,
395 records,
396 rejections,
397 postcodes,
398 } = emitted;
399 self.report.merge(report);
400 self.postcodes.merge(postcodes);
401 for rejection in &rejections {
402 self.audit.write(rejection)?;
403 }
404 for record in records {
405 self.push(record, None)?;
406 }
407 Ok(())
408 }
409
410 fn push(&mut self, record: Record, origin: Option<ContextOrigin>) -> Result<()> {
411 let [lon, lat] = record
412 .display_point()
413 .with_context(|| format!("record {} has no display point", record.id()))?;
414 let pending = PendingRecord {
415 key: hilbert_key(quantize(lon)?, quantize(lat)?),
416 seq: self.seq,
417 origin,
418 record,
419 };
420 self.seq += 1;
421 if pending.record.layer().is_context() {
422 self.context_records.push(pending)
423 } else {
424 self.records.push(pending)
425 }
426 }
427}
428
429fn request_nodes(
430 requests: &mut ExternalSorter<NodeRequest>,
431 owner: u64,
432 refs: &[i64],
433 want_tags: bool,
434) -> Result<()> {
435 for (pos, node_id) in refs.iter().enumerate() {
436 requests.push(NodeRequest {
437 node_id: *node_id,
438 owner,
439 pos: u32::try_from(pos).context("way has too many nodes")?,
440 want_tags,
441 })?;
442 }
443 Ok(())
444}
445
446fn member_ways(block: &osmpbf::PrimitiveBlock, member_ids: &HashSet<i64>) -> Vec<(i64, Vec<i64>)> {
447 block
448 .elements()
449 .filter_map(|element| match element {
450 osmpbf::Element::Way(way) if member_ids.contains(&way.id()) => {
451 Some((way.id(), way.refs().collect()))
452 }
453 _ => None,
454 })
455 .collect()
456}
457
458fn write_ordered(
461 writer: &mut PackWriter,
462 records: impl Iterator<Item = Result<PendingRecord>>,
463 total: u64,
464 index: &BoundaryIndex,
465 message: &'static str,
466) -> Result<()> {
467 let progress = item_progress_bar(total, message);
468 let mut records = records.peekable();
469 while records.peek().is_some() {
470 let batch = records
471 .by_ref()
472 .take(WRITE_BATCH_RECORDS)
473 .collect::<Result<Vec<_>>>()?;
474 let batch = batch
475 .into_par_iter()
476 .map(|pending| {
477 let context: Option<RecordContext> = match pending.origin {
478 Some(_) => None,
479 None => record_context(index, &pending.record),
480 };
481 (pending.record, context)
482 })
483 .collect::<Vec<_>>();
484 writer.write_batch(&batch)?;
485 progress.inc(batch.len() as u64);
486 }
487 progress.finish_with_message(format!("{message} complete"));
488 Ok(())
489}
490
491struct Audit {
493 dir: PathBuf,
494 rejections: BufWriter<File>,
495 count: u64,
496}
497
498impl Audit {
499 fn create(generation: &Path) -> Result<Self> {
500 let dir = generation.join(AUDIT_DIR);
501 fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?;
502 let path = dir.join(REJECTIONS_FILE);
503 let file =
504 File::create(&path).with_context(|| format!("failed to create {}", path.display()))?;
505 Ok(Self {
506 dir,
507 rejections: BufWriter::with_capacity(1 << 20, file),
508 count: 0,
509 })
510 }
511
512 fn write(&mut self, rejection: &RejectedRecord) -> Result<()> {
513 serde_json::to_writer(&mut self.rejections, rejection)?;
514 self.rejections.write_all(b"\n")?;
515 self.count += 1;
516 Ok(())
517 }
518
519 fn finish(mut self, report: &BuilderReport) -> Result<()> {
520 self.rejections.flush()?;
521 let path = self.dir.join(BUILD_REPORT_FILE);
522 let file =
523 File::create(&path).with_context(|| format!("failed to create {}", path.display()))?;
524 serde_json::to_writer_pretty(BufWriter::new(file), report)
525 .with_context(|| format!("failed to write {}", path.display()))
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use crate::{
532 pack::PackReader,
533 reverse::{PackReverseGeocoder, ReverseGeocodeOptions, ReverseMatchKind},
534 search::{PackTextSearcher, TextSearchOptions},
535 };
536
537 use super::{
538 test_pbf::{TestElement, node, relation, way, write_pbf},
539 *,
540 };
541
542 fn temp_dir(name: &str) -> PathBuf {
543 let dir = std::env::temp_dir().join(format!(
544 "open-geocode-build-{name}-{}",
545 uuid::Uuid::new_v4()
546 ));
547 fs::create_dir_all(&dir).expect("dir");
548 dir
549 }
550
551 fn town() -> Vec<Vec<TestElement>> {
555 vec![
556 vec![
557 node(
558 1,
559 43.60,
560 -79.40,
561 &[
562 ("addr:housenumber", "10"),
563 ("addr:street", "King Street"),
564 ("addr:city", "Toronto"),
565 ("addr:postcode", "M5V 1A1"),
566 ],
567 ),
568 node(
569 2,
570 43.6001,
571 -79.4001,
572 &[
573 ("addr:housenumber", "12"),
574 ("addr:street", "King Street"),
575 ("addr:postcode", "m5v 1a1"),
576 ("addr:city", "Toronto"),
578 ("addr:state", "ON"),
579 ("addr:country", "CA"),
580 ],
581 ),
582 node(3, 43.6002, -79.4002, &[("addr:housenumber", "14")]),
583 node(
584 4,
585 43.67,
586 -79.40,
587 &[("place", "neighbourhood"), ("name", "Annex")],
588 ),
589 node(20, 43.61, -79.42, &[]),
590 node(21, 43.61, -79.41, &[]),
591 node(30, 43.6200, -79.4300, &[]),
592 node(31, 43.6200, -79.4298, &[]),
593 node(32, 43.6202, -79.4298, &[]),
594 node(33, 43.6202, -79.4300, &[]),
595 ],
596 vec![
597 node(
598 40,
599 43.630,
600 -79.44,
601 &[("addr:housenumber", "2"), ("addr:street", "Elm Street")],
602 ),
603 node(
604 41,
605 43.631,
606 -79.44,
607 &[("addr:housenumber", "10"), ("addr:street", "Elm Street")],
608 ),
609 node(1001, 43.5, -79.5, &[]),
610 node(1002, 43.5, -79.3, &[]),
611 node(1003, 43.7, -79.3, &[]),
612 node(1004, 43.7, -79.5, &[]),
613 node(1011, 43.0, -80.0, &[]),
614 node(1012, 43.0, -79.0, &[]),
615 node(1013, 44.0, -79.0, &[]),
616 node(1014, 44.0, -80.0, &[]),
617 ],
618 vec![
619 way(500, &[1001, 1002, 1003], &[]),
620 way(501, &[1003, 1004, 1001], &[]),
621 way(502, &[1011, 1012, 1013, 1014, 1011], &[]),
622 way(
623 600,
624 &[20, 21],
625 &[("highway", "residential"), ("name", "Queen Street")],
626 ),
627 way(601, &[20, 21], &[("highway", "service")]),
628 way(
629 602,
630 &[30, 31, 32, 33, 30],
631 &[
632 ("building", "yes"),
633 ("addr:housenumber", "5"),
634 ("addr:street", "Bay Street"),
635 ],
636 ),
637 way(
638 603,
639 &[40, 41],
640 &[
641 ("addr:interpolation", "even"),
642 ("addr:street", "Elm Street"),
643 ],
644 ),
645 way(
646 604,
647 &[20, 9999],
648 &[("highway", "residential"), ("name", "Ghost Road")],
649 ),
650 way(
652 605,
653 &[30, 31, 32, 33, 30],
654 &[
655 ("boundary", "administrative"),
656 ("admin_level", "10"),
657 ("name", "Bay Ward"),
658 ],
659 ),
660 ],
661 vec![
662 relation(
663 900,
664 &[(500, "outer"), (501, "outer")],
665 &[
666 ("boundary", "administrative"),
667 ("admin_level", "8"),
668 ("name", "Toronto"),
669 ],
670 ),
671 relation(
672 902,
673 &[(605, "outer")],
674 &[
675 ("boundary", "administrative"),
676 ("admin_level", "10"),
677 ("name", "Bay Ward Relation"),
678 ],
679 ),
680 relation(
681 901,
682 &[(502, "outer")],
683 &[
684 ("boundary", "administrative"),
685 ("admin_level", "4"),
686 ("name", "Ontario"),
687 ("ISO3166-2", "CA-ON"),
688 ],
689 ),
690 ],
691 ]
692 }
693
694 fn build(root: &Path, input: &Path, memory_budget_bytes: usize) -> BuilderReport {
695 build_osm_pack(BuildOsmOptions {
696 input: input.to_path_buf(),
697 pack: root.to_path_buf(),
698 memory_budget_bytes,
699 scratch_dir: None,
700 })
701 .expect("build")
702 }
703
704 #[test]
705 fn builds_a_searchable_reverse_geocodable_pack_from_pbf() {
706 let dir = temp_dir("town");
707 let input = dir.join("town.osm.pbf");
708 write_pbf(&input, &town());
709 let report = build(&dir.join("pack"), &input, DEFAULT_MEMORY_BUDGET_BYTES);
710
711 let reader = PackReader::open(dir.join("pack")).expect("reader");
712 let counts = &reader.manifest().layer_counts;
713 let count = |layer: &str| counts.get(layer).copied().unwrap_or_default();
714 assert_eq!(
715 count("address"),
716 5,
717 "four nodes (two are anchors) and one building"
718 );
719 assert_eq!(count("street"), 1);
720 assert_eq!(count("interpolation"), 1);
721 assert_eq!(count("postcode"), 1);
722 assert_eq!(
723 count("neighbourhood"),
724 3,
725 "Annex, the Bay Ward way and its relation"
726 );
727 assert_eq!(count("locality"), 1);
728 assert_eq!(count("region"), 1);
729 assert_eq!(count("country"), 1, "derived from ISO3166-2");
730 assert_eq!(report.output.record_count, 14);
731 assert_eq!(report.geometry_resolution.required_node_refs, 27);
734 assert_eq!(report.accepted.postcode_records, 1);
735 assert_eq!(report.geometry_resolution.street_way_stubs, 2);
736 assert_eq!(
737 report.scratch.sorters["records"].runs, 0,
738 "a small input never spills"
739 );
740 assert!(report.scratch.peak_tracked_bytes > 0);
741 assert!(report.scratch.peak_tracked_bytes <= report.scratch.memory_budget_bytes);
742
743 let headers = (0..reader.manifest().record_count)
745 .map(|id| reader.records().header(id).expect("header"))
746 .collect::<Vec<_>>();
747 let first_other = headers
748 .iter()
749 .position(|header| !header.layer.is_context())
750 .expect("non-context records");
751 assert!(
752 headers[..first_other]
753 .iter()
754 .all(|header| header.layer.is_context())
755 );
756 assert!(
757 headers[first_other..]
758 .iter()
759 .all(|header| !header.layer.is_context())
760 );
761 for group in [&headers[..first_other], &headers[first_other..]] {
762 let keys = group
763 .iter()
764 .map(|header| hilbert_key(header.lon_e7, header.lat_e7))
765 .collect::<Vec<_>>();
766 assert!(keys.windows(2).all(|pair| pair[0] <= pair[1]));
767 }
768
769 let king = reader
771 .find_by_source_id("osm:node:1")
772 .expect("lookup")
773 .expect("address node 1");
774 let context = reader
775 .boundary_context(king)
776 .expect("context")
777 .expect("has context");
778 let name = |id: Option<u64>| {
779 reader
780 .context_record(id.expect("id"))
781 .expect("record")
782 .expect("context")
783 .name
784 };
785 assert_eq!(name(context.admin_context.locality_record_id), "Toronto");
786 assert_eq!(name(context.admin_context.region_record_id), "Ontario");
787 assert_eq!(name(context.admin_context.country_record_id), "Canada");
788
789 let searcher = PackTextSearcher::open(dir.join("pack")).expect("searcher");
790 let hits = searcher
791 .search(TextSearchOptions {
792 query: "10 King Street".into(),
793 limit: 1,
794 layer: Some("address".into()),
795 })
796 .expect("search");
797 assert_eq!(hits[0].record.id, "osm:node:1");
798 assert_eq!(hits[0].record.label, "10 King Street, Toronto, M5V 1A1");
799
800 let reverse = PackReverseGeocoder::open(dir.join("pack")).expect("reverse");
801 let result = |lon, lat| {
802 reverse
803 .reverse(ReverseGeocodeOptions { lon, lat })
804 .expect("reverse")
805 .result
806 .expect("result")
807 };
808 let building = result(-79.4299, 43.6201);
809 assert_eq!(building.match_kind, ReverseMatchKind::ExplicitAddress);
810 assert_eq!(building.id.as_deref(), Some("osm:way:602"));
811 let estimated = result(-79.44001, 43.6305);
812 assert_eq!(estimated.match_kind, ReverseMatchKind::EstimatedAddress);
813 assert!(
814 estimated.label.starts_with("6 Elm Street"),
815 "{}",
816 estimated.label
817 );
818 let street = result(-79.415, 43.61001);
819 assert_eq!(street.match_kind, ReverseMatchKind::NearestStreet);
820 assert!(
821 street.label.starts_with("Queen Street, ") && street.label.contains("Toronto"),
822 "{}",
823 street.label
824 );
825
826 let rejections = reader.rejections(0).expect("rejections");
828 let reasons = rejections
829 .iter()
830 .map(|rejection| rejection.reason.as_str())
831 .collect::<Vec<_>>();
832 assert!(reasons.contains(&"missing_street_or_place"), "{reasons:?}");
833 assert!(
834 reasons.contains(&"street_unresolved_geometry"),
835 "{reasons:?}"
836 );
837 assert!(
838 !reader
839 .section_sizes()
840 .keys()
841 .any(|name| name.contains("reject"))
842 );
843 assert_eq!(report.output.rejection_count, rejections.len() as u64);
844 let generation = reader.path().parent().expect("generation");
845 assert!(generation.join(AUDIT_DIR).join(BUILD_REPORT_FILE).is_file());
846
847 let bench = crate::bench::benchmark_pack(crate::bench::PackBenchmarkOptions {
849 pack: dir.join("pack"),
850 queries: None,
851 iterations: 1,
852 warmup: 0,
853 })
854 .expect("bench");
855 let build_metrics = bench.pack.build.expect("build metrics");
856 assert_eq!(build_metrics.accepted_records, report.accepted.total);
857 assert_eq!(build_metrics.scratch, report.scratch);
858 }
859
860 #[test]
861 fn spilling_to_disk_builds_the_same_pack() {
862 let dir = temp_dir("spill");
863 let input = dir.join("town.osm.pbf");
864 write_pbf(&input, &town());
865 let in_memory = build(&dir.join("memory"), &input, DEFAULT_MEMORY_BUDGET_BYTES);
866 let spilled = build(&dir.join("spilled"), &input, 4);
869 let spilling_sorters = spilled
870 .scratch
871 .sorters
872 .values()
873 .filter(|sorter| sorter.runs > 0)
874 .count();
875 assert!(spilling_sorters >= 4, "{:?}", spilled.scratch);
876 assert!(spilled.scratch.spilled_bytes > 0);
877 assert_eq!(in_memory.accepted, spilled.accepted);
878 assert_eq!(in_memory.rejected, spilled.rejected);
879 assert_eq!(in_memory.triage, spilled.triage);
880
881 let memory = PackReader::open(dir.join("memory")).expect("memory pack");
882 let spilled = PackReader::open(dir.join("spilled")).expect("spilled pack");
883 assert_eq!(
884 memory.manifest().record_count,
885 spilled.manifest().record_count
886 );
887 for id in 0..memory.manifest().record_count {
888 assert_eq!(
889 memory.record_json(id).expect("memory record"),
890 spilled.record_json(id).expect("spilled record")
891 );
892 assert_eq!(
893 memory.boundary_context(id).expect("memory context"),
894 spilled.boundary_context(id).expect("spilled context")
895 );
896 }
897 }
898
899 #[test]
900 fn rejects_inputs_whose_nodes_are_not_sorted() {
901 let dir = temp_dir("unsorted");
902 let input = dir.join("unsorted.osm.pbf");
903 write_pbf(
904 &input,
905 &[
906 vec![node(5, 43.6, -79.4, &[]), node(3, 43.6, -79.4, &[])],
907 vec![way(
908 10,
909 &[3, 5],
910 &[("highway", "residential"), ("name", "Loop")],
911 )],
912 ],
913 );
914 let error = build_osm_pack(BuildOsmOptions {
915 input,
916 pack: dir.join("pack"),
917 ..BuildOsmOptions::default()
918 })
919 .unwrap_err();
920 assert!(format!("{error:#}").contains("osmium sort"), "{error:#}");
921 assert!(!dir.join("pack").join("CURRENT").exists());
922 }
923}