The problem
A raw NOTAM looks like this:
Q) CYHZ/QMRLC/IV/NBO/A/000/999/4453N06331W005
A) CYHZ B) 2602011200 C) 2602012359
E) RWY 05/23 CLSD
Feed that to an LLM and you get creative confabulation. The Q-code alone encodes five dimensions — FIR, subject, condition, traffic, purpose, scope — and there are 400+ valid subject/condition combinations in FAA Order 7930.2 Appendix B. Aviation adds ~300 further abbreviations (RWY, TWY, APCH, WEF, UFN) that the model will guess at.
The model doesn't need to learn Q-codes. The FAA already wrote the rulebook.
Approach
- Full Q-code lookup covering every subject and condition code in Appendix B.
- Abbreviation expansion dictionary (~300 terms), scoped so it doesn't mangle acronyms that also appear as words.
- Two parsers — ICAO format (the Q-code variant above) and FAA domestic format.
- Structured JSON output, so the decoder sits in front of any downstream LLM or API consumer.
Architecture
Three core modules, no external dependencies:
parser.py— format-aware parser that extracts Q-code fields, times, locations, and the E-field body text.qcodes.py— lookup tables (subject codes, condition codes, traffic codes) as straightforward dicts.abbreviations.py— abbreviation expansion with contextual guards.
Plus a cli.py wrapper for standalone use. Public API: NOTAMParser, decode_qcode, expand_abbreviations_clean.
Status & links
Shipping. Zero external dependencies — pure stdlib with a pytest suite. Integrated as a submodule inside Travel Alerts MCP, where it preprocesses FAA feeds before they reach the downstream LLM.