1use std::{
16 collections::BTreeMap,
17 fs,
18 io::{BufRead, BufReader},
19 path::{Path, PathBuf},
20 sync::Arc,
21 time::{SystemTime, UNIX_EPOCH},
22};
23
24use anyhow::{Context, Result, bail};
25use rayon::prelude::*;
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28
29use crate::{
30 container::{Container, ContainerWriter},
31 context::{ContextReader, ContextTupleWriter},
32 extsort::Scratch,
33 memory::MemoryBudget,
34 record::{AddressRecord, InterpolationRecord, Layer, Record, RejectedRecord, StreetRecord},
35 records::{RecordsReader, RecordsWriter},
36 spatial_index::{RecordCells, SpatialIndexWriter},
37 text_index::TextIndexWriter,
38};
39
40pub use crate::{
41 context::{AdminContextTuple, RecordContext},
42 records::{ContextRecord, RecordPoint, RecordPointPrecision, RecordSource, RecordSummary},
43};
44
45pub type RecordId = u64;
46
47mod publication;
48pub use publication::resolve_pack_path;
49
50pub const PACK_FILE: &str = "pack.ogp";
51pub const AUDIT_DIR: &str = "audit";
52pub const REJECTIONS_FILE: &str = "rejections.jsonl";
53pub const BUILD_REPORT_FILE: &str = "build-report.json";
54pub const PACK_SCHEMA_VERSION: u32 = 6;
55
56const MANIFEST_SECTION: &str = "manifest";
57const MANIFEST_VERSION: u32 = 1;
58const SCRATCH_DIR: &str = ".scratch";
59const DEFAULT_SORT_BUDGET_BYTES: usize = 512 << 20;
61const DEFAULT_TEXT_INDEX_MEMORY_BYTES: usize = 1 << 30;
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub struct PackManifest {
65 pub schema_version: u32,
66 pub crate_version: String,
67 pub built_at_unix: u64,
68 pub record_count: u64,
69 pub layer_counts: BTreeMap<String, u64>,
70 pub string_count: u64,
71 pub context_tuple_count: u64,
72 pub text_document_count: u64,
73 pub spatial: PackSpatialManifest,
74}
75
76#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
77pub struct PackSpatialManifest {
78 pub point_count: u64,
79 pub segment_count: u64,
80 pub cell_count: u64,
81 pub context_cell_count: u64,
82}
83
84#[derive(Debug, Clone)]
85pub struct PackWriterOptions {
86 pub scratch_dir: Option<PathBuf>,
89 pub memory: Arc<MemoryBudget>,
91 pub sort_budget_bytes: usize,
93 pub text_index_memory_bytes: usize,
95}
96
97impl Default for PackWriterOptions {
98 fn default() -> Self {
99 Self {
100 scratch_dir: None,
101 memory: MemoryBudget::unlimited(),
102 sort_budget_bytes: DEFAULT_SORT_BUDGET_BYTES,
103 text_index_memory_bytes: DEFAULT_TEXT_INDEX_MEMORY_BYTES,
104 }
105 }
106}
107
108pub struct PackWriter {
110 destination: PathBuf,
111 generation: GenerationGuard,
112 scratch: Arc<Scratch>,
113 records: RecordsWriter,
114 contexts: ContextTupleWriter,
115 text: TextIndexWriter,
116 spatial: SpatialIndexWriter,
117 layer_counts: BTreeMap<Layer, u64>,
118}
119
120pub struct SealedPack {
122 destination: PathBuf,
123 generation: GenerationGuard,
124 manifest: PackManifest,
125 bytes: u64,
126}
127
128struct GenerationGuard {
130 path: PathBuf,
131 keep: bool,
132}
133
134impl Drop for GenerationGuard {
135 fn drop(&mut self) {
136 if !self.keep {
137 let _ = fs::remove_dir_all(&self.path);
138 }
139 }
140}
141
142#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
144pub struct PackBuildStats {
145 pub bytes: u64,
146 pub sections: BTreeMap<String, u64>,
147 pub spatial_pair_runs: u64,
148 pub text_index_bytes: u64,
149}
150
151impl PackWriter {
152 pub fn create(destination: impl AsRef<Path>) -> Result<Self> {
153 Self::create_with(destination, PackWriterOptions::default())
154 }
155
156 pub fn create_with(destination: impl AsRef<Path>, options: PackWriterOptions) -> Result<Self> {
157 let destination = destination.as_ref().to_path_buf();
158 let generation = GenerationGuard {
159 path: publication::create_generation(&destination)?,
160 keep: false,
161 };
162 let scratch = Scratch::create(
163 options
164 .scratch_dir
165 .map(|dir| dir.join(generation.path.file_name().expect("generation name")))
166 .unwrap_or_else(|| generation.path.join(SCRATCH_DIR)),
167 )?;
168 Ok(Self {
169 records: RecordsWriter::create(&scratch, &options.memory)?,
170 contexts: ContextTupleWriter::default(),
171 text: TextIndexWriter::create(
172 &scratch,
173 &options.memory,
174 options.text_index_memory_bytes,
175 )?,
176 spatial: SpatialIndexWriter::new(&scratch, &options.memory, options.sort_budget_bytes),
177 scratch,
178 destination,
179 generation,
180 layer_counts: BTreeMap::new(),
181 })
182 }
183
184 pub fn scratch(&self) -> &Arc<Scratch> {
186 &self.scratch
187 }
188
189 pub fn generation(&self) -> &Path {
192 &self.generation.path
193 }
194
195 pub fn record_count(&self) -> u64 {
196 self.records.record_count()
197 }
198
199 pub fn write(&mut self, record: &Record, context: Option<RecordContext>) -> Result<RecordId> {
200 let first = self.records.record_count();
201 self.write_batch(std::slice::from_ref(&(record.clone(), context)))?;
202 Ok(first)
203 }
204
205 pub fn write_batch(&mut self, batch: &[(Record, Option<RecordContext>)]) -> Result<()> {
208 let first = self.records.record_count();
209 let fields = self.text.fields();
210 let prepared = batch
211 .par_iter()
212 .enumerate()
213 .map(|(offset, (record, _))| {
214 let record_id = first + offset as u64;
215 Ok((
216 fields.document(record_id, record),
217 RecordCells::for_record(record_id, record)?,
218 ))
219 })
220 .collect::<Result<Vec<_>>>()?;
221 for ((record, context), (document, cells)) in batch.iter().zip(prepared) {
222 let context = match context {
223 Some(context) => self.contexts.intern(*context)?,
224 None => None,
225 };
226 self.records.write(record, context)?;
227 self.text.add(document)?;
228 self.spatial.add(cells)?;
229 *self.layer_counts.entry(record.layer()).or_default() += 1;
230 }
231 Ok(())
232 }
233
234 pub fn seal(self) -> Result<(SealedPack, PackBuildStats)> {
236 let Self {
237 destination,
238 generation,
239 scratch,
240 records,
241 contexts,
242 text,
243 spatial,
244 layer_counts,
245 } = self;
246 let path = generation.path.join(PACK_FILE);
247 let mut pack = ContainerWriter::create(&path)?;
248 let record_count = records.record_count();
249 let string_count = records.string_count();
250 let context_tuple_count = contexts.tuple_count();
251
252 records.finish(&mut pack)?;
253 contexts.finish(&mut pack)?;
254 let text_commit = text.finish(&mut pack)?;
255 let spatial_commit = spatial.finish(&mut pack)?;
256
257 let manifest = PackManifest {
258 schema_version: PACK_SCHEMA_VERSION,
259 crate_version: env!("CARGO_PKG_VERSION").to_string(),
260 built_at_unix: SystemTime::now()
261 .duration_since(UNIX_EPOCH)
262 .map(|duration| duration.as_secs())
263 .unwrap_or_default(),
264 record_count,
265 layer_counts: layer_counts
266 .iter()
267 .map(|(layer, count)| (layer.as_str().to_string(), *count))
268 .collect(),
269 string_count,
270 context_tuple_count,
271 text_document_count: text_commit.document_count,
272 spatial: PackSpatialManifest {
273 point_count: spatial_commit.point_count,
274 segment_count: spatial_commit.segment_count,
275 cell_count: spatial_commit.cell_count,
276 context_cell_count: spatial_commit.context_cell_count,
277 },
278 };
279 pack.add(
280 MANIFEST_SECTION,
281 MANIFEST_VERSION,
282 &serde_json::to_vec_pretty(&manifest)?,
283 )?;
284 let bytes = pack.finish()?;
285 drop(scratch);
287
288 let sections = Container::open(&path)?
289 .sections()
290 .iter()
291 .map(|(name, info)| (name.clone(), info.len))
292 .collect();
293 Ok((
294 SealedPack {
295 destination,
296 generation,
297 manifest,
298 bytes,
299 },
300 PackBuildStats {
301 bytes,
302 sections,
303 spatial_pair_runs: spatial_commit.fine_pairs.runs
304 + spatial_commit.context_pairs.runs,
305 text_index_bytes: text_commit.bytes,
306 },
307 ))
308 }
309
310 pub fn finish(self) -> Result<PackManifest> {
312 self.seal()?.0.publish()
313 }
314}
315
316impl SealedPack {
317 pub fn generation(&self) -> &Path {
318 &self.generation.path
319 }
320
321 pub fn manifest(&self) -> &PackManifest {
322 &self.manifest
323 }
324
325 pub fn bytes(&self) -> u64 {
326 self.bytes
327 }
328
329 pub fn publish(mut self) -> Result<PackManifest> {
331 publication::publish(
332 &self.destination,
333 &self.generation.path,
334 &self.manifest,
335 &mut self.generation.keep,
336 )?;
337 Ok(self.manifest)
338 }
339}
340
341pub struct PackReader {
342 path: PathBuf,
343 container: Container,
344 manifest: PackManifest,
345 records: RecordsReader,
346 context: ContextReader,
347}
348
349impl std::fmt::Debug for PackReader {
350 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351 formatter
352 .debug_struct("PackReader")
353 .field("path", &self.path)
354 .field("manifest", &self.manifest)
355 .finish_non_exhaustive()
356 }
357}
358
359impl PackReader {
360 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
362 let path = resolve_pack_path(path)?;
363 let container = Container::open(&path)?;
364 let manifest: PackManifest =
365 serde_json::from_slice(&container.section(MANIFEST_SECTION, MANIFEST_VERSION)?)
366 .with_context(|| format!("failed to parse the manifest of {}", path.display()))?;
367 if manifest.schema_version != PACK_SCHEMA_VERSION {
368 bail!(
369 "Pack schema version {} is unsupported; rebuild the Pack for schema {}",
370 manifest.schema_version,
371 PACK_SCHEMA_VERSION
372 );
373 }
374 let records = RecordsReader::open(&container)?;
375 if records.record_count() != manifest.record_count {
376 bail!(
377 "record store has {} records but the manifest declares {}",
378 records.record_count(),
379 manifest.record_count
380 );
381 }
382 let context = ContextReader::open(&container)?;
383 Ok(Self {
384 path,
385 container,
386 manifest,
387 records,
388 context,
389 })
390 }
391
392 pub const fn manifest(&self) -> &PackManifest {
393 &self.manifest
394 }
395
396 pub fn path(&self) -> &Path {
398 &self.path
399 }
400
401 pub fn container(&self) -> &Container {
402 &self.container
403 }
404
405 pub fn verify(&self) -> Result<()> {
407 self.container
408 .verify()
409 .with_context(|| format!("{} failed verification", self.path.display()))
410 }
411
412 pub fn records(&self) -> &RecordsReader {
413 &self.records
414 }
415
416 pub fn section_sizes(&self) -> BTreeMap<String, u64> {
418 self.container
419 .sections()
420 .iter()
421 .map(|(name, info)| (name.clone(), info.len))
422 .collect()
423 }
424
425 pub fn record_summary(&self, record_id: RecordId) -> Result<RecordSummary> {
426 self.records.summary(record_id)
427 }
428
429 pub fn record_json(&self, record_id: RecordId) -> Result<Value> {
430 self.records.record_json(record_id)
431 }
432
433 pub fn records_json_by_layer(&self, layer: &str, limit: usize) -> Result<Vec<Value>> {
434 let layer =
435 Layer::parse(layer).with_context(|| format!("unknown record layer: {layer}"))?;
436 let mut records = Vec::new();
437 for record_id in 0..self.records.record_count() {
438 if self.records.header(record_id)?.layer != layer {
439 continue;
440 }
441 records.push(self.record_json(record_id)?);
442 if limit > 0 && records.len() >= limit {
443 break;
444 }
445 }
446 Ok(records)
447 }
448
449 pub fn find_by_source_id(&self, source_id: &str) -> Result<Option<RecordId>> {
450 for record_id in 0..self.records.record_count() {
451 if self.record_summary(record_id)?.id == source_id {
452 return Ok(Some(record_id));
453 }
454 }
455 Ok(None)
456 }
457
458 pub fn address(&self, record_id: RecordId) -> Result<Option<AddressRecord>> {
459 Ok(match self.records.record(record_id)? {
460 Record::Address(record) => Some(record),
461 _ => None,
462 })
463 }
464
465 pub fn interpolation(&self, record_id: RecordId) -> Result<Option<InterpolationRecord>> {
466 if self.records.header(record_id)?.layer != Layer::Interpolation {
467 return Ok(None);
468 }
469 Ok(match self.records.record(record_id)? {
470 Record::Interpolation(record) => Some(record),
471 _ => None,
472 })
473 }
474
475 pub fn street(&self, record_id: RecordId) -> Result<Option<StreetRecord>> {
476 if self.records.header(record_id)?.layer != Layer::Street {
477 return Ok(None);
478 }
479 Ok(match self.records.record(record_id)? {
480 Record::Street(record) => Some(record),
481 _ => None,
482 })
483 }
484
485 pub fn context_record(&self, record_id: RecordId) -> Result<Option<ContextRecord>> {
486 self.records.context_record(record_id)
487 }
488
489 pub fn boundary_context(&self, record_id: RecordId) -> Result<Option<RecordContext>> {
490 self.records
491 .header(record_id)?
492 .context
493 .map(|reference| self.context.resolve(reference))
494 .transpose()
495 }
496
497 pub fn rejections(&self, limit: usize) -> Result<Vec<RejectedRecord>> {
499 let path = self
500 .path
501 .parent()
502 .map(|dir| dir.join(AUDIT_DIR).join(REJECTIONS_FILE))
503 .context("Pack file has no parent directory")?;
504 let file = fs::File::open(&path)
505 .with_context(|| format!("failed to open build audit {}", path.display()))?;
506 let mut rejections = Vec::new();
507 for line in BufReader::new(file).lines() {
508 if limit > 0 && rejections.len() >= limit {
509 break;
510 }
511 rejections.push(serde_json::from_str(&line?).context("invalid rejection record")?);
512 }
513 Ok(rejections)
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use std::io::Write;
520
521 use crate::{
522 record::{
523 AddressComponents, LocationPrecision, OsmObjectType, SourceProvenance, point_geometry,
524 },
525 search::{PackTextSearcher, TextSearchOptions},
526 };
527
528 use super::*;
529
530 fn temp_root(name: &str) -> PathBuf {
531 std::env::temp_dir().join(format!("open-geocode-{name}-{}", uuid::Uuid::new_v4()))
532 }
533
534 fn address_record(object_id: i64, number: &str, street: &str) -> Record {
535 Record::Address(AddressRecord {
536 address: AddressComponents {
537 number: number.to_string(),
538 street: Some(street.to_string()),
539 place: None,
540 unit: None,
541 locality: None,
542 region: None,
543 postcode: None,
544 country: None,
545 },
546 geometry: point_geometry(-79.0, 43.0),
547 location_precision: LocationPrecision::Point,
548 source: SourceProvenance::osm(OsmObjectType::Node, object_id),
549 })
550 }
551
552 fn write_pack(root: &Path, records: &[Record]) -> PackManifest {
553 let mut writer = PackWriter::create(root).expect("writer");
554 for record in records {
555 writer.write(record, None).expect("write");
556 }
557 writer.finish().expect("finish")
558 }
559
560 fn first_hit(root: &Path, query: &str) -> Option<String> {
561 PackTextSearcher::open(root)
562 .expect("searcher")
563 .search(TextSearchOptions {
564 query: query.into(),
565 limit: 1,
566 layer: None,
567 })
568 .expect("search")
569 .first()
570 .map(|hit| hit.record.id.clone())
571 }
572
573 #[test]
574 fn writes_one_file_and_reads_records_by_row_layer_and_source_id() {
575 let root = temp_root("pack");
576 let manifest = write_pack(
577 &root,
578 &[
579 address_record(1, "10", "King Street"),
580 address_record(2, "20", "Queen Street"),
581 ],
582 );
583 assert_eq!(manifest.record_count, 2);
584 assert_eq!(manifest.text_document_count, 2);
585 assert_eq!(manifest.spatial.point_count, 2);
586
587 let reader = PackReader::open(&root).expect("reader");
588 assert_eq!(
589 reader.path().file_name().and_then(|name| name.to_str()),
590 Some(PACK_FILE)
591 );
592 assert_eq!(reader.record_summary(1).expect("row 1").id, "osm:node:2");
593 assert_eq!(
594 reader.find_by_source_id("osm:node:1").expect("lookup"),
595 Some(0)
596 );
597 assert_eq!(
598 reader
599 .records_json_by_layer("address", 10)
600 .expect("layer")
601 .len(),
602 2
603 );
604 assert!(
605 reader
606 .records_json_by_layer("street", 10)
607 .expect("layer")
608 .is_empty()
609 );
610 assert!(reader.records_json_by_layer("nope", 10).is_err());
611 let sections = reader.section_sizes();
612 assert!(sections.contains_key("records/blocks"));
613 assert!(sections.keys().any(|name| name.starts_with("text/")));
614 assert!(sections.contains_key("spatial/fine/data"));
615 assert_eq!(first_hit(&root, "queen").as_deref(), Some("osm:node:2"));
616
617 let copy = temp_root("copy").with_extension("ogp");
619 fs::copy(reader.path(), ©).expect("copy");
620 assert_eq!(first_hit(©, "king").as_deref(), Some("osm:node:1"));
621 assert!(!reader.path().parent().unwrap().join(SCRATCH_DIR).exists());
622 }
623
624 #[test]
625 fn failed_osm_rebuild_preserves_published_pack() {
626 let root = temp_root("rebuild");
627 write_pack(&root, &[address_record(1, "10", "King Street")]);
628 let generations = fs::read_dir(root.join("generations")).expect("dir").count();
629
630 let result = crate::builder::build_osm_pack(crate::builder::BuildOsmOptions {
631 input: root.join("missing.osm.pbf"),
632 pack: root.clone(),
633 ..crate::builder::BuildOsmOptions::default()
634 });
635 assert!(result.is_err());
636 assert_eq!(first_hit(&root, "king").as_deref(), Some("osm:node:1"));
637 assert_eq!(
638 fs::read_dir(root.join("generations")).expect("dir").count(),
639 generations,
640 "the failed generation is removed"
641 );
642 }
643
644 #[test]
645 fn publishes_replacement_without_changing_existing_readers() {
646 let root = temp_root("publish");
647 write_pack(&root, &[address_record(1, "10", "King Street")]);
648 let original = PackReader::open(&root).expect("original reader");
649 let searcher = PackTextSearcher::open(&root).expect("original searcher");
650
651 let mut replacement = PackWriter::create(&root).expect("replacement");
652 replacement
653 .write(&address_record(2, "20", "Queen Street"), None)
654 .expect("address");
655 assert_eq!(first_hit(&root, "king").as_deref(), Some("osm:node:1"));
656 replacement.finish().expect("publish");
657
658 assert_eq!(first_hit(&root, "queen").as_deref(), Some("osm:node:2"));
659 assert_eq!(
660 original.record_summary(0).expect("original").id,
661 "osm:node:1"
662 );
663 let hits = searcher
664 .search(TextSearchOptions {
665 query: "king".into(),
666 limit: 5,
667 layer: None,
668 })
669 .expect("existing searcher after publish");
670 assert_eq!(hits[0].record.id, "osm:node:1");
671 }
672
673 #[test]
674 fn validation_failure_does_not_publish_replacement() {
675 let root = temp_root("validation");
676 write_pack(&root, &[address_record(1, "10", "King Street")]);
677 let published = fs::read(root.join("CURRENT")).expect("pointer");
678
679 let mut replacement = PackWriter::create(&root).expect("replacement");
680 replacement
681 .write(&address_record(2, "20", "Queen Street"), None)
682 .expect("address");
683 let (sealed, _) = replacement.seal().expect("seal");
684 let generation = sealed.generation().to_path_buf();
685 let pack = generation.join(PACK_FILE);
687 let mut bytes = fs::read(&pack).expect("read");
688 let len = bytes.len();
689 bytes[len - 1] ^= 0xff;
690 fs::File::create(&pack)
691 .and_then(|mut file| file.write_all(&bytes))
692 .expect("corrupt");
693 assert!(sealed.publish().is_err());
694 assert_eq!(fs::read(root.join("CURRENT")).expect("pointer"), published);
695 assert!(!generation.exists());
696 assert_eq!(first_hit(&root, "king").as_deref(), Some("osm:node:1"));
697 }
698
699 #[test]
700 fn publishing_refuses_a_pack_corrupted_inside_a_section() {
701 let root = temp_root("checksum");
702 write_pack(&root, &[address_record(1, "10", "King Street")]);
703 let published = fs::read(root.join("CURRENT")).expect("pointer");
704
705 let mut replacement = PackWriter::create(&root).expect("replacement");
706 replacement
707 .write(&address_record(2, "20", "Queen Street"), None)
708 .expect("address");
709 let (sealed, _) = replacement.seal().expect("seal");
710 let pack = sealed.generation().join(PACK_FILE);
711 let offset = Container::open(&pack).expect("open").sections()["records/strings"].offset;
712 let mut bytes = fs::read(&pack).expect("read");
713 let last = bytes.len() - 1;
714 bytes[(offset as usize + 20).min(last)] ^= 0x55;
715 fs::write(&pack, &bytes).expect("corrupt");
716
717 let error = sealed.publish().unwrap_err();
718 assert!(format!("{error:#}").contains("checksum"), "{error:#}");
719 assert_eq!(fs::read(root.join("CURRENT")).expect("pointer"), published);
720 }
721
722 #[test]
723 fn a_failure_after_the_pointer_switch_keeps_the_published_generation() {
724 let root = temp_root("switch-failure");
725 write_pack(&root, &[address_record(1, "10", "King Street")]);
726 let mut replacement = PackWriter::create(&root).expect("replacement");
727 replacement
728 .write(&address_record(2, "20", "Queen Street"), None)
729 .expect("address");
730 let (sealed, _) = replacement.seal().expect("seal");
731
732 publication::FAIL_AFTER_SWITCH.set(true);
733 let result = sealed.publish();
734 publication::FAIL_AFTER_SWITCH.set(false);
735 assert!(result.is_err());
736 assert_eq!(first_hit(&root, "queen").as_deref(), Some("osm:node:2"));
738 }
739
740 #[test]
741 fn text_index_memory_fits_the_budget_and_waits_for_records() {
742 let root = temp_root("text-memory");
743 let memory = MemoryBudget::new(64 << 20);
744 let mut writer = PackWriter::create_with(
745 &root,
746 PackWriterOptions {
747 scratch_dir: None,
748 memory: Arc::clone(&memory),
749 sort_budget_bytes: 16 << 20,
750 text_index_memory_bytes: 16 << 20,
751 },
752 )
753 .expect("writer");
754 assert_eq!(
755 memory.used(),
756 0,
757 "nothing is reserved before records arrive"
758 );
759 writer
760 .write(&address_record(1, "10", "King Street"), None)
761 .expect("address");
762 assert!(memory.used() > 0);
763 assert!(
764 memory.peak() <= memory.limit(),
765 "peak {} of {}",
766 memory.peak(),
767 memory.limit()
768 );
769 writer.finish().expect("finish");
770 assert_eq!(first_hit(&root, "king").as_deref(), Some("osm:node:1"));
771 }
772
773 #[test]
774 fn rejects_paths_without_a_pack() {
775 let root = temp_root("empty");
776 fs::create_dir_all(&root).expect("dir");
777 assert!(PackReader::open(&root).is_err());
778 }
779}