1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//! # Event matcher
//!
//! A Rust library for matching event records following the
//! [schema.org/Event](https://schema.org/Event) data model. The crate
//! implements both **deterministic** and **probabilistic** matching
//! algorithms.
//!
//! The library is **deterministic**, **stateless**, **panic-free** in
//! library code, and **`Send + Sync`** so it can be used freely across
//! threads.
//!
//! ## What it does
//!
//! Given two [`Event`] records — typically drawn from different source
//! systems — the [`MatchingEngine`] decides whether they refer to the
//! same event. The output is either a hard boolean (deterministic) or a
//! scored [`MatchResult`] with a per-field [`matcher::MatchBreakdown`] so
//! an auditor or downstream system can inspect the decision.
//!
//! ## Crate layout
//!
//! | Module | Purpose |
//! |---|---|
//! | [`models`] | Data types: [`Event`], [`EventBuilder`], [`Address`], [`Location`], [`EventCategory`], [`EventStatus`], [`EventAttendanceMode`], [`EventId`], [`EventIdScheme`]. |
//! | [`normalizer`] | Text and ISO 8601 normalisation: names, postcodes, addresses, phonetic codes, date-times. |
//! | [`scorer`] | String, geographic, and temporal similarity primitives (Jaro-Winkler, Levenshtein, Haversine, Gaussian decay). |
//! | [`matcher`] | Orchestration: [`MatchingEngine`], [`MatchConfig`], [`MatchResult`]. |
//! | [`error`] | Error enum [`MatchingError`] and [`Result`] alias. |
//!
//! ## Quick start — probabilistic match
//!
//! ```
//! use event_matcher::{MatchingEngine, MatchConfig, Event};
//!
//! let a = Event::builder()
//! .name("Glastonbury Festival 2024")
//! .add_alternate_name("Glasto 2024")
//! .start_date("2024-06-26T09:00:00Z")
//! .end_date("2024-06-30T23:59:00Z")
//! .build();
//!
//! let b = Event::builder()
//! .name("Glasto 2024")
//! .start_date("2024-06-26T09:15:00Z")
//! .end_date("2024-06-30T23:59:00Z")
//! .build();
//!
//! let engine = MatchingEngine::new(MatchConfig::default());
//! let result = engine.match_events(&a, &b);
//!
//! assert!(result.is_match);
//! ```
//!
//! ## Inspecting the per-field breakdown
//!
//! Every probabilistic match returns a per-field score so the decision is
//! auditable end-to-end. Missing or unparseable fields score `None` rather
//! than zero — they do not penalise the event.
//!
//! ```
//! use event_matcher::{MatchingEngine, Event};
//!
//! let p = Event::builder()
//! .name("RustConf 2024")
//! .start_date("2024-09-10T09:00:00Z")
//! .build();
//! let q = p.clone();
//!
//! let result = MatchingEngine::default_config().match_events(&p, &q);
//! assert!(result.breakdown.name_score.unwrap() > 0.99);
//! assert!(result.breakdown.start_date_score.unwrap() > 0.99);
//! ```
//!
//! ## Configuration presets
//!
//! Three configurations cover most use cases. Use [`MatchConfig::strict`]
//! when callers must rely on the answer; use [`MatchConfig::lenient`]
//! to triage large candidate sets where false negatives are worse than
//! false positives.
//!
//! ```
//! use event_matcher::{MatchConfig, MatchingEngine};
//!
//! let strict = MatchingEngine::new(MatchConfig::strict());
//! let default = MatchingEngine::default_config();
//! let lenient = MatchingEngine::new(MatchConfig::lenient());
//!
//! // All three engines share the same scoring pipeline; only the
//! // threshold and a couple of weights differ.
//! # let _ = (strict, default, lenient);
//! ```
//!
//! ## Determinism and safety
//!
//! - **Deterministic.** Same inputs => same outputs. No clocks, no RNGs,
//! no environment variables.
//! - **No `unsafe`.** This crate forbids `unsafe` code.
//! - **No IO.** The library does not log, read files, or open sockets.
//! - **No panics** in library code paths; every fallible input returns
//! `None` from a scorer or a [`MatchingError`].
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;