Skip to main content

open_geocode/
context.rs

1//! Boundary-derived admin context.
2//!
3//! Most records share their admin context with many neighbours, so contexts are
4//! interned as tuples of place record ids. A record body stores only the tuple
5//! id ([`ContextRef`]); this module stores the tuple table.
6//!
7//! ```text
8//! context/tuples  tuple_count u64, then per tuple 6 x u32 record ids
9//!                 (country, region, district, locality, neighbourhood, place),
10//!                 u32::MAX for absent
11//! ```
12
13use std::collections::HashMap;
14
15use anyhow::{Context, Result, bail};
16use serde::{Deserialize, Serialize};
17
18use crate::{
19    container::{Bytes, Container, ContainerWriter},
20    pack::RecordId,
21    records::ContextRef,
22    util::codec::{read_u32_le, read_u64_le},
23};
24
25pub const SECTION_TUPLES: &str = "context/tuples";
26pub const CONTEXT_VERSION: u32 = 1;
27pub const CONTEXT_FLAG_AMBIGUOUS_ADMIN: u16 = 1;
28
29const TUPLE_BYTES: usize = 24;
30const MISSING_ID: u32 = u32::MAX;
31
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct AdminContextTuple {
34    pub country_record_id: Option<RecordId>,
35    pub region_record_id: Option<RecordId>,
36    pub district_record_id: Option<RecordId>,
37    pub locality_record_id: Option<RecordId>,
38    pub neighbourhood_record_id: Option<RecordId>,
39    pub place_record_id: Option<RecordId>,
40}
41
42/// Admin context of one record plus assignment flags.
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
44pub struct RecordContext {
45    pub admin_context: AdminContextTuple,
46    pub flags: u16,
47}
48
49impl AdminContextTuple {
50    pub fn is_empty(self) -> bool {
51        self.ids().iter().all(Option::is_none)
52    }
53
54    /// Most specific first: place, neighbourhood, locality, district, region,
55    /// country.
56    pub fn parent_record_ids(self) -> impl Iterator<Item = RecordId> {
57        let [country, region, district, locality, neighbourhood, place] = self.ids();
58        [place, neighbourhood, locality, district, region, country]
59            .into_iter()
60            .flatten()
61    }
62
63    fn ids(self) -> [Option<RecordId>; 6] {
64        [
65            self.country_record_id,
66            self.region_record_id,
67            self.district_record_id,
68            self.locality_record_id,
69            self.neighbourhood_record_id,
70            self.place_record_id,
71        ]
72    }
73
74    fn from_ids(ids: [Option<RecordId>; 6]) -> Self {
75        let [
76            country_record_id,
77            region_record_id,
78            district_record_id,
79            locality_record_id,
80            neighbourhood_record_id,
81            place_record_id,
82        ] = ids;
83        Self {
84            country_record_id,
85            region_record_id,
86            district_record_id,
87            locality_record_id,
88            neighbourhood_record_id,
89            place_record_id,
90        }
91    }
92}
93
94#[derive(Debug, Default)]
95pub struct ContextTupleWriter {
96    tuples: Vec<AdminContextTuple>,
97    ids: HashMap<AdminContextTuple, u32>,
98}
99
100impl ContextTupleWriter {
101    /// Reference for a record's context; `None` when there is no admin context.
102    pub fn intern(&mut self, context: RecordContext) -> Result<Option<ContextRef>> {
103        if context.admin_context.is_empty() {
104            return Ok(None);
105        }
106        let tuple_id = match self.ids.get(&context.admin_context) {
107            Some(id) => *id,
108            None => {
109                for id in context.admin_context.ids().into_iter().flatten() {
110                    if id >= u64::from(MISSING_ID) {
111                        bail!("context record id {id} does not fit the context table");
112                    }
113                }
114                let id = u32::try_from(self.tuples.len()).context("context table is full")?;
115                self.tuples.push(context.admin_context);
116                self.ids.insert(context.admin_context, id);
117                id
118            }
119        };
120        Ok(Some(ContextRef {
121            tuple_id,
122            ambiguous: context.flags & CONTEXT_FLAG_AMBIGUOUS_ADMIN != 0,
123        }))
124    }
125
126    pub fn tuple_count(&self) -> u64 {
127        self.tuples.len() as u64
128    }
129
130    pub fn finish(self, pack: &mut ContainerWriter) -> Result<()> {
131        let mut bytes = Vec::with_capacity(8 + self.tuples.len() * TUPLE_BYTES);
132        bytes.extend_from_slice(&(self.tuples.len() as u64).to_le_bytes());
133        for tuple in &self.tuples {
134            for id in tuple.ids() {
135                let id = id.map_or(MISSING_ID, |id| id as u32);
136                bytes.extend_from_slice(&id.to_le_bytes());
137            }
138        }
139        pack.add(SECTION_TUPLES, CONTEXT_VERSION, &bytes)
140    }
141}
142
143#[derive(Clone)]
144pub struct ContextReader {
145    tuples: Bytes,
146    count: u64,
147}
148
149impl ContextReader {
150    pub fn open(container: &Container) -> Result<Self> {
151        let tuples = container.section(SECTION_TUPLES, CONTEXT_VERSION)?;
152        let count = read_u64_le(&tuples, 0).context("context table is truncated")?;
153        if tuples.len() as u64 != 8 + count * TUPLE_BYTES as u64 {
154            bail!("context table does not match its tuple count");
155        }
156        Ok(Self { tuples, count })
157    }
158
159    pub fn resolve(&self, reference: ContextRef) -> Result<RecordContext> {
160        let id = u64::from(reference.tuple_id);
161        if id >= self.count {
162            bail!("context tuple {id} is out of range");
163        }
164        let offset = 8 + id as usize * TUPLE_BYTES;
165        let ids = std::array::from_fn(|index| {
166            let value = read_u32_le(&self.tuples, offset + index * 4).expect("validated table");
167            (value != MISSING_ID).then_some(RecordId::from(value))
168        });
169        Ok(RecordContext {
170            admin_context: AdminContextTuple::from_ids(ids),
171            flags: if reference.ambiguous {
172                CONTEXT_FLAG_AMBIGUOUS_ADMIN
173            } else {
174                0
175            },
176        })
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn interns_tuples_and_resolves_references() {
186        let path =
187            std::env::temp_dir().join(format!("open-geocode-context-{}", uuid::Uuid::new_v4()));
188        let tuple = AdminContextTuple {
189            country_record_id: Some(1),
190            region_record_id: Some(2),
191            locality_record_id: Some(3),
192            ..AdminContextTuple::default()
193        };
194        let mut writer = ContextTupleWriter::default();
195        let first = writer
196            .intern(RecordContext {
197                admin_context: tuple,
198                flags: 0,
199            })
200            .expect("intern")
201            .expect("reference");
202        let second = writer
203            .intern(RecordContext {
204                admin_context: tuple,
205                flags: CONTEXT_FLAG_AMBIGUOUS_ADMIN,
206            })
207            .expect("intern")
208            .expect("reference");
209        assert_eq!(first.tuple_id, second.tuple_id);
210        assert!(second.ambiguous);
211        assert_eq!(
212            writer.intern(RecordContext::default()).expect("empty"),
213            None
214        );
215        assert_eq!(writer.tuple_count(), 1);
216
217        let mut pack = ContainerWriter::create(&path).expect("pack");
218        writer.finish(&mut pack).expect("finish");
219        pack.finish().expect("pack finish");
220        let reader = ContextReader::open(&Container::open(&path).expect("open")).expect("reader");
221        let resolved = reader.resolve(second).expect("resolve");
222        assert_eq!(resolved.admin_context, tuple);
223        assert_eq!(resolved.flags, CONTEXT_FLAG_AMBIGUOUS_ADMIN);
224        assert_eq!(
225            resolved
226                .admin_context
227                .parent_record_ids()
228                .collect::<Vec<_>>(),
229            vec![3, 2, 1]
230        );
231    }
232}