Two Heuristics and an Authority: Designing for the Moment Your Matchers Disagree
- #pattern
- #reconciliation
- #matching
- #human-in-the-loop
- #data-quality
A video channel and a website drift apart the way any two hand-maintained systems drift apart. Videos get published. Some get linked into the site, some do not, and after a year nobody can tell you which is which. Finding the gap is trivial — a set difference over identifiers. The hard part is the next question: where does each orphaned video belong?
This is a note about the pattern that answered it, not the code that implements it. The implementation is welded to one channel’s quota budget and one account’s authorization flow; there is nothing generic to hand you and no repository to link. The shape, though, generalises to any reconciliation problem where you have several imperfect signals and an expensive mistake.
The problem is placement, not detection
The site organises a multi-day route into day sections — day one, day two, and so on, each with a date, a heading, and a list of the places it passes through. An orphaned video has to land in one of those sections. Guess wrong and the error is not a warning in a log: it is published content, wrong on a page a reader sees, and unwinding it costs another edit, another build, another review pass. The asymmetry between “correct” and “wrong” is the whole design constraint.
So the goal was never a matcher with the best accuracy. It was a system that knows when it doesn’t know.
Three signals that cannot fail the same way
Three matchers run over every unplaced video, independently. Independence is the point — a majority vote among signals that share a failure mode is just one signal wearing a disguise.
- Chronological. Assign the video to the day whose date is the latest one on or before the publication date, on the theory that footage is published on or after the day it was shot. Cheap, no lookups, and it degrades honestly: if a video appears more than a month after the route ended it is a post-production release, the signal carries nothing, and the matcher returns nothing rather than pinning everything to the final day.
- Text overlap. Tokenize the title and description, drop a stop list of filler — generic geography, direction words, production vocabulary like “scenic” or “footage” — and score what remains against each day’s known place names and heading. Distinctive words dominate because the boring ones were removed first. Output is a day plus a score between zero and one, so weak matches are visibly weak.
- Geographic, and authoritative. If the platform carried recorded coordinates for the clip, use them. If not, parse endpoint names out of the title — the “Harrow Bay to Kestrel Pass” shape — and resolve each through a geocoding service. Either way you end up with points, and each point is compared against the recorded track for every day, keeping the nearest. A day earns a hit for each point landing within a few kilometres of its own track; the winner is the day with the most hits, ties broken by aggregate distance.
The third one is different in kind, and the code says so out loud: use actual geography, not title-text guesswork. The first two infer. The third measures. It is also the slow one — network calls, a polite delay between geocoder requests — which is exactly why it does not run alone.
Agreement is the licence to act
The consensus rule is small enough to state in full:
1if chrono_day and text_day agree -> assign that day
2if chrono has no signal and text is strong -> assign the text day
3otherwise -> assign nothing; flag it
Two things are worth noticing. The strong-text fallback exists because “no signal” and “wrong signal” are different states, and the chronological matcher was built to distinguish them — a matcher that returns nothing when it has nothing is what makes a fallback safe. And “strong” is a threshold on a real score, not a vibe: below it, a lone matcher does not get to decide anything.
Everything else lands in the second pile.
Disagreement gets a person, not an average
The tempting moves here are all wrong in the same way.
- Average the votes. There is no evidence for the day in the middle. Averaging manufactures a confident answer out of a genuine conflict and destroys the one piece of information you had — that your signals disagreed.
- Trust the highest confidence. This buries the conflict rather than resolving it. The authority is right more often, not always; when it contradicts both heuristics, that is the interesting case, not the settled one.
- Pick a tiebreak rule and move on. A tiebreak is a coin flip wearing a policy hat, and it will be invisible in the output.
So the third option is to stop. Disagreement is surfaced, not resolved: the report splits proposals into auto-assignable and needs-review, and every flagged item shows what each matcher said, including the score. The geographic pass writes a full evidence chain per video — every endpoint it parsed, what it resolved to, which day it landed nearest, how far away, and whether that was close enough to count. A reviewer audits the reasoning, not just the verdict.
The reconciliation is deliberately generous about admitting confusion in the other direction too. Identifiers linked on the site that no longer appear in the channel get their own section, and a diagnostic pass can ask what actually happened to each one — deleted, made private, or simply outside the current filter. Three different causes with three different responses; collapsing them into “missing” would have been the same mistake as averaging.
Deciding and applying are separate programs
The report generator never writes to the site. That is not a convention, it is a separate entry point with no write path: it reads, it renders markdown, it exits. A second program reads the proposals, walks a human through them one at a time — defaulting to the consensus answer where there is one, demanding a choice where there is not — and only then edits the page. It also has a dry run that prints the plan and writes nothing.
The split buys two things that a single command cannot have at once. The slow, networked, quota-consuming stage can run unattended on a schedule, dropping a report that is empty on a quiet week and interesting on a busy one. And the irreversible stage — the one that mutates published content — always has a person in it, holding a report that was generated before anyone was in a hurry.
There is one more small honesty mechanism worth stealing: when the applier edits a page that had been marked as human-reviewed, it clears that flag. The machine cannot quietly inherit a human’s sign-off.
The part that travels
Strip out the videos and the routes and this is what is left:
- Use several signals that fail for different reasons, and make each one return “no answer” rather than a bad answer when its assumptions do not hold.
- Let agreement, not confidence, be the trigger for acting automatically.
- Route disagreement to a person. Never to arithmetic.
- Keep the deciding stage read-only and the applying stage interactive, in separate programs.
- Carry the evidence with the proposal, so review is auditing rather than re-deriving.
None of that is sophisticated. It is the same instinct as preferring mechanical enforcement to behavioural rules: decide in advance what the system is allowed to do on its own, and make the ambiguous case structurally unable to slip through as if it were the clear one.
The reflex worth building is the one that treats disagreement among your own signals as a finding rather than a nuisance. Whenever a wrong answer is expensive to unwind, “stop and ask” beats “average the votes” — and it beats it by a margin that grows with how confident your averaging was.
Configuration details reflect a production environment at time of writing. Implementation specifics vary based on tooling versions, platform updates, and organizational requirements. Validate approaches against current documentation before deployment.