Reverse frequency lookup for SDR. Click a signal, learn what it is.
Every authoritative source for spectrum allocation is a PDF, a wall chart, or a Word document laid out for print. None of them can be queried by frequency. This project parses them into a database that can, so a receiver can show something useful when you tap a peak on the waterfall.
$ python3 tools/lookup.py 162.55
── 162.55 MHz ──────────────────────────────────────────────────
Known use
• [US] NOAA Weather Radio WX1 [Weather]
162.537 MHz – 162.562 MHz NFM
NOAA All Hazards continuous broadcast; NWS forecast and warning audio.
source: NWS / NTIA Redbook
• [EU] VHF PMR (CEPT) [Land Mobile]
146 MHz – 174 MHz NFM/DMR
European private mobile radio; 12.5 kHz narrowband. Note 2 m amateur ends at 146.
source: ERC/REC 70-03 / national
United States allocation
United States (Federal)
162.037 MHz – 173.2 MHz: FIXED, MOBILE
footnotes: US8, US11, US13, US55, US73, US300, US312, G5
United States (non-Federal)
162.037 MHz – 173.2 MHz: no service listed
footnotes: US8, US11, US13, US55, US73, US300, US312
International allocation (Region 1 = Europe, Africa, Middle East)
ITU Region 1
162.037 MHz – 174 MHz: FIXED, MOBILE except aeronautical mobile
ITU Regions 2–3
162.037 MHz – 174 MHz: FIXED, MOBILE
FCC rule parts
Part 74D — Remote Pickup
Part 90 — Private Land Mobile
Results are ranked narrowest first, so the recognisable name leads. Restrict the
curated layer with --jurisdiction us or --jurisdiction eu; add --footnotes
for the full text of every footnote that applies.
| Allocation bands | 2,666 across 5 jurisdictions |
| Coverage | 0 Hz to 275 GHz, no gaps |
| Footnotes | 973, with full text |
| Rule part links | 594 |
| Curated channels | 368 (238 US, 130 European) |
| Source revision | FCC Online Table, 3 April 2026 |
| Largest artifact | 1.6 MB SQLite; 135 KB flat binary |
| Dependencies | none, at build time or runtime |
The example above is the entire design argument. The statutory allocation table answers 162.55 MHz with FIXED, MOBILE. That is correct, and it is worthless to somebody staring at a waterfall. The name anybody actually wants, NOAA Weather Radio, appears nowhere in the allocation table.
So the database carries two layers and ranks results by specificity.
| Layer | Question it answers | Where it comes from | Size |
|---|---|---|---|
| Allocation | Which service class owns this band? | 47 CFR 2.106, parsed | 2,666 bands |
| Known use | What would I actually hear here? | Curated per rule part and standard | 368 channels |
A lookup returns both, narrowest first, so the recognisable name leads and the statutory allocation sits underneath it. Because the allocation layer has no gaps, a query inside 0 Hz to 275 GHz can never come back empty.
There is deliberately no third layer for licensees. FCC ULS will tell you which station holds 155.475 MHz in your county, but that is millions of rows, it depends on location, and it would dominate the repository. It belongs in a separate project.
| id | Meaning | Layer |
|---|---|---|
itu-r1 |
ITU Region 1: Europe, Africa, Middle East, former USSR | Allocation |
itu-r2 |
ITU Region 2: the Americas | Allocation |
itu-r3 |
ITU Region 3: Asia-Pacific | Allocation |
us-federal |
US Federal table, administered by NTIA | Allocation |
us-non-federal |
US non-Federal table, administered by the FCC | Allocation |
us |
Curated US channel plans | Known use |
eu |
Curated European (CEPT) channel plans | Known use |
Regional differences are real and routinely get flipped by US-authored data. Several are pinned by tests.
| Region 1 (Europe) | United States | |
|---|---|---|
| 2 m amateur band | 144 to 146 MHz | 144 to 148 MHz |
| 70 cm amateur band | 430 to 440 MHz | 420 to 450 MHz |
| Amateur at 432–438 MHz | Primary | Secondary |
| 433 MHz | ISM band | Part 15 use inside the amateur band |
| FM broadcast | 87.5 to 108 MHz | 88 to 108 MHz |
| MW broadcast raster | 9 kHz | 10 kHz |
| VHF airband spacing | 8.33 kHz mandatory | 25 kHz |
European curated coverage: PMR446, the full DAB Band III block raster, 868 MHz SRD and LoRaWAN EU868, 433 MHz SRD and LPD433, TETRA, GSM-R, wireless M-Bus, Z-Wave EU, DCF77 and MSF, and the CEPT cellular band pairings.
Europe currently gets the international allocation layer, which is ITU Region 1, rather than a national statutory table. Nothing here encodes a Dutch or German licensing condition. Closing that would mean parsing the CEPT ECC European Common Allocation Table, a separate document of comparable size.
A mis-parse of a print layout looks exactly like a correct one at a glance, so correctness is checked structurally. The Allocation Table partitions the spectrum: within any one jurisdiction, each band must begin precisely where its predecessor ended.
itu-r1 bands= 543 gaps= 0 overlaps= 0 span=0 Hz-275 GHz
itu-r2 bands= 543 gaps= 0 overlaps= 0 span=0 Hz-275 GHz
itu-r3 bands= 537 gaps= 0 overlaps= 0 span=0 Hz-275 GHz
us-federal bands= 501 gaps= 0 overlaps= 0 span=0 Hz-275 GHz
us-non-federal bands= 542 gaps= 0 overlaps= 0 span=0 Hz-275 GHz
That single check catches almost any column misassignment, because a cell put in the wrong column leaves a hole where it belonged and collides where it landed. Alongside it sit spot checks against allocations verifiable from other sources: FM broadcast, the 2 m amateur band, marine Channel 16, AIS 1 and 2, GPS L1, ADS-B.
41 tests run across the three languages.
| Suite | Tests | Command |
|---|---|---|
| Parser invariants and known frequencies | 25 | make test-python |
| C reader, including malformed input | 8 | make test-c |
| Swift wrapper | 8 | make test-swift |
Python 3.11 or newer and a C99 compiler. Nothing else.
make # parse the source and build every artifact
make test # all three suites
make lookup FREQ=1090Artifacts land in build/.
| File | Size | Use it when |
|---|---|---|
rfalloc.sqlite |
1.6 MB | You want everything: footnote text, rule parts, raw source cells |
rfalloc.json |
1.7 MB | Same content, no SQLite dependency |
rfalloc.min.json |
351 KB | Lookup only, for a web front end |
rfalloc.bin |
135 KB | Embedding in an app via the C or Swift reader |
import RFAlloc
let db = try SpectrumDatabase(contentsOf: url) // rfalloc.bin
db.summary(at: 162_550_000) // "NOAA Weather Radio WX1"
db.channels(at: 446_006_250).first?.name // "PMR446 Channel 1"
// A waterfall wants everything across the visible span, not one point.
db.channels(in: 144_000_000 ..< 148_000_000)
// Region 1 is Europe. All three ITU regions are present.
db.allocations(at: 433_920_000).filter { $0.jurisdiction == .ituRegion1 }Add it as a package dependency, or drag c/rfalloc.c and c/rfalloc.h straight
into an Xcode target.
Queries are local, read-only, and allocation-free inside the reader, so calling them from a rendering path is fine. A tooltip never triggers a network request.
#include "rfalloc.h"
rfalloc_db db;
if (rfalloc_open(&db, bytes, len) != RFALLOC_OK) { /* ... */ }
rfalloc_channel hits[8];
int n = rfalloc_channels_at(&db, 162550000ULL, hits, 8);
printf("%s\n", hits[0].name); /* NOAA Weather Radio WX1 */c/rfalloc.c is 211 lines of C99 with no dependency beyond the standard
library. It allocates nothing, reads scalars byte by byte so that unaligned
loads and byte order are non-issues, and validates every header offset against
the file size before dereferencing anything. Vendor the two files anywhere.
Frequencies are integer hertz. Floating-point megahertz makes band-edge comparison unreliable in precisely the place where correctness matters.
Ranges are half-open, [lo, hi). Bands in the source abut exactly, as in
148–149.9 followed by 149.9–150.05. A closed upper bound would make every
boundary frequency match two bands at once.
Jurisdiction is a table, never a column. One source document already yields
five jurisdictions, so the schema was multi-national from the first commit.
Adding an administration to the curated layer means creating a directory under
data/curated/; the directory name is the jurisdiction.
Lookups are range-overlap queries. Hovering gives you a point, but a spectrum display wants every allocation across the visible span so it can draw a band ribbon beneath the trace.
Nothing is lossy. Every band keeps the verbatim source cell in raw_text,
so parsing choices can be revisited without going back to the document.
sources/ FCC source documents, kept for reproducible builds
tools/ parser and build pipeline, Python 3.11+, no dependencies
data/curated/
us/ US channel plans, one CSV per service
eu/ European (CEPT) channel plans
c/ C99 reader
swift/ Swift wrapper around the C reader
docs/ parsing methodology, schema reference
Prebuilt artifacts are attached to every release,
so you do not need Python or a compiler to use the data. Tagging v* builds and
publishes them automatically, with checksums.
Code under MIT. Data under CC0, so the tables can be embedded in a commercial product with no conditions attached.
The derived data is public domain because works of the United States Government carry no copyright under 17 U.S.C. 105. LICENSE-DATA sets out the reasoning and the one rule that must never be broken: no imports from RadioReference or any other proprietary frequency directory, not even partially. Those compilations are licensed, and a single import would make the whole database undistributable.
The curated layer is where help is worth most, and it needs no code. Add a row
to a CSV under data/curated/<jurisdiction>/ with a public source citation.
CONTRIBUTING.md has the rules; docs/PARSING.md
covers how the parse works and what is already known to be imperfect.
| Document | |
|---|---|
| docs/PARSING.md | How a print layout is turned into data, and the known limitations |
| docs/SCHEMA.md | Table reference and example queries |
| sources/README.md | Provenance of the source documents |
| FCC Online Table of Frequency Allocations | Upstream source |
| 47 CFR 2.106 on eCFR | The codified table |
| NTIA Redbook | Federal use, beyond what 2.106 records |
| CEPT ECC EFIS | European allocation data |