cqrlog-cnty-files
DXCC tables for CQRLOG
- Active · last change 9-24-2026
- ★ 2 on GitHub
README
cqrlog-cnty-files
DXCC country files and the parser that reads them.
The data is Martin OK1RR's country file set, which has driven CQRLOG's DXCC resolution for twenty years. The parser is a modernised implementation of the engine that consumes it, proven to resolve callsigns identically to the original before being separated out.
The point of keeping them together is that neither is much use without the other, and both are useful outside CQRLOG.
data/ the country files
dxcc/ what the parser reads
extra/ small tables CQRLOG needs and the parser does not
spec/ the format and the algorithm, language-neutral
parsers/
pascal/ the reference implementation (Free Pascal)
python/ a conforming port (Python 3.9+, stdlib only)
go/ a conforming port (Go 1.21+, stdlib only)
tools/ feeding CQRLOG itself, and building a distributable archive
packaging/ the README that ships inside that archive
docs/ how the equivalence was established, and open data issuesQuick start
You need fpc (Free Pascal 3.2+). No Lazarus, no LCL, no database, and no
data preparation step.
cd parsers/pascal
make test # 125 tests, ~6 s
make lookup # build the command-line toolExamples
Resolve one callsign
$ tools/dxcclookup --call OK1AYY --date 1992-12-12 --explain
callsign : OK1AYY
date : 1992/12/12
country : Czechoslovakia
adif : 218
status : deleted entity
key : OK1AYY
pattern : OK
continent: EU
cq/itu : 15 / 28
lat/lon : 49N 20E
valid : 1945/01/01 - 1992/12/31
table : country_del.tabThe same callsign today is a different entity, which is the whole reason the date is not optional:
$ tools/dxcclookup --call OK1AYY --date 2026-08-19
country : Czech Republic, Full License
adif : 503Argentine provinces live in the fourth character of the callsign, and a trailing
letter moves them — /F is Santa Fe, applied by rewriting character 4:
$ tools/dxcclookup --call LU1AAA/F --date 2020-01-01 --explain
country : Argentina, Santa Fe (SF)
adif : 100
key : LU1FAA
pattern : LU#FResolve a whole log
$ tools/dxcclookup --csv qsos.csv
date,callsign,key,adif,country
2020/01/15,OK1ABC,OK1ABC,503,"Czech Republic, Full License"
1992/12/12,OK1AYY,OK1AYY,218,Czechoslovakia
2020/01/01,LU1AAA/F,LU1FAA,100,"Argentina, Santa Fe (SF)"
2020/01/01,OK1ABC/MM,?,,
SUMMARY rows=4 nocountry=1 unparseable=0Input is date,callsign with an optional header — the shape you get exporting
qsodate and callsign from a CQRLOG log. The ? key on the last row is
maritime mobile: no country, by design. The summary goes to stderr, so the CSV
on stdout stays pipeable.
Use it from Pascal
The whole API, end to end. Note that there is no file to generate first:
BuildCountryTable reads the OK1RR files and merges them in memory.
uses uDxccTable, uDxccEntry, uDxccResolver, uDxccSuffixRules, uDxccSource;
var
Valid, Deleted: TDxccTable;
Rules: TDxccSuffixRules;
Resolver: TDxccResolver;
Key: string;
Resolved: Boolean;
Adif, Idx: Integer;
begin
Valid := TDxccTable.Create;
Valid.LoadFromString(BuildCountryTable('data/dxcc'));
Deleted := TDxccTable.Create;
Deleted.LoadFromString(BuildDeletedTable('data/dxcc'));
Rules := TDxccSuffixRules.Create;
Rules.LoadExceptions('data/dxcc/Exceptions.tab');
Rules.LoadAmbiguous('data/dxcc/Ambiguous.tbl');
Resolver := TDxccResolver.Create(Valid, Deleted, Rules);
{ OK1ABC/P -> OK1ABC; KL7AA/1 -> W1; LU1AAA/F -> LU1FAA }
Key := Resolver.EffectiveCallsign('OK1ABC/P', '2020/01/01', Resolved, Adif);
{ Deleted entities are searched first. }
Idx := Deleted.Find(Key, '2020/01/01', dmPrefix);
if Idx = -1 then
Idx := Valid.Find(Key, '2020/01/01', dmPrefix);
if Idx <> -1 then
Writeln(Valid.Entry(Idx).Country);
end;TDxccTable is immutable once loaded, so any number of threads may call Find
on one concurrently. To reload, build a new instance and swap the reference.
Reading a line by hand
Line 4476 of AreaOK1RR.tbl, verbatim:
OK1[A-Z]% OK1[A-JL-NPQS-Z]%% OK[2-7][A-Z]% OK[2-7][A-JL-NPQS-Z]%%|Czech Republic, Full License|EU|-1|50.07N|14.42E|28|15||R|2005/05/01-=503Everything before the first | is marks — four of them here, space
separated. Each becomes its own entry in the index, all sharing one
description. OK1[A-JL-NPQS-Z]%% matches OK1, then one letter from those
ranges, then two more alphanumerics: so OK1AYY, but not OK1KYY.
Everything after is the description:
| Field | Value | Note |
|---|---|---|
| country | Czech Republic, Full License |
what the log displays |
| continent | EU |
|
| UTC offset | -1 |
|
| latitude | 50.07N |
WGS84 decimal degrees, not min/sec |
| longitude | 14.42E |
|
| ITU zone | 28 |
|
| CQ zone | 15 |
|
| ADIF column | (empty) | supplied by the =503 below instead |
| flag | R |
D here would mean a deleted entity |
| validity | 2005/05/01- |
open-ended; the default end is 2050/01/01 |
| ADIF | 503 |
from the =503 suffix |
That 2005/05/01 start is why OK1AYY resolves differently either side of it,
and — because a shorter wildcard mark outranks the finer ones before that date —
it is also the cause of the open data issue listed under Status.
Full detail in spec/FORMAT.md.
Writing a parser in another language
This is what the repository is arranged for. A conforming Python port exists
in parsers/python/ — its README doubles as a porting
guide, including the one representation decision that matters (bytes in,
Latin-1 throughout) — and a Go port in parsers/go/, whose
Engine is shaped for embedding in a service. A PHP port is the intended
next one.
- Read spec/FORMAT.md and spec/ALGORITHM.md. Between them they specify the whole thing, with no reference to Pascal.
- Implement it, reading
data/dxcc/directly. Assembling the tables is about twenty lines — concatenate three files in a fixed order, optionally expand the%-leading lines, drop bytes below 32. There is deliberately no preprocessing step to depend on. - Check it against the Pascal one.
tools/dxcclookup --csvtakes adate,callsignfile and prints what it resolved, so running the same input through both and diffing the output is the whole comparison. A log export makes a good input; so does a list of callsigns built from the table's own patterns.
A sketch of step 2, to show the spec is implementable without reading any Pascal:
from pathlib import Path
def build_country_table(d: Path, expand: bool = True) -> list[str]:
lines = []
# The order matters: the sort is stable and ties go to the first entry.
for name in ("Country.tab", "CallResolution.tbl", "AreaOK1RR.tbl"):
raw = (d / name).read_bytes()
# Drop every byte below 32 -- this is what makes the CRLF files load
# the same as the LF ones.
text = bytes(b for b in raw if b >= 32 or b == 0x0A).decode("utf-8", "replace")
lines.extend(text.splitlines())
if expand:
# A '%'-leading mark sorts above every alphanumeric and can never be
# reached by a lookup keyed on the callsign's first character, so each
# is republished under every letter. Generated lines are not re-expanded.
lines += [c + ln[1:] for ln in lines if ln.startswith("%")
for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"]
return linesTo check your merge is right, compare against tools/build-tables.sh, which
writes the same content out as files. (The sketch is an illustration, not the
byte-exact recipe: for output that compares byte for byte with
--dump-tables, strip CR only during the merge and leave the below-32 filter
to table loading, and do not decode with errors="replace" — see
parsers/python/dxcc/source.py and the "Bytes and encoding" section of
parsers/python/README.md for the exact rules.)
The data
data/dxcc/ is Martin OK1RR's set, dropped in verbatim — same bytes, same line
endings. Refreshing it is a plain copy. See data/README.md for
provenance, per-file notes and the rule about replacing all files at once.
data/extra/ holds sat_name.tab, prop_mode.tab, ContestName.tab and
iota.tbl: CQRLOG reads them, the parser does not.
Not included: lotw1.txt, eqsl.txt, MASTER.SCP and qslmgr.csv. They are
LoTW/eQSL membership lists, a super-check-partial list and a QSL manager
database — none of them take part in resolving a callsign, and each churns tens
of thousands of lines per refresh. They stay with CQRLOG. Three of the four are
fetched at build time instead of being stored — see below.
Building a country file archive
tools/build-cty-archive.sh assembles cqrlog-cty.tar.gz, the fifteen-file set
CQRLOG unpacks into ~/.config/cqrlog/ctyfiles. Eleven members come straight out
of data/; lotw1.txt, eqsl.txt and MASTER.SCP are fetched from their live
sources, which is exactly why they need not be committed.
tools/build-cty-archive.sh --out build/cty # GNU tar required; on macOS TAR=gtarEvery member is validated before it is packed, so a truncated download or an
HTML error page fails the build rather than shipping. The archive is
reproducible: the same sources and the same --version-date give identical
bytes.
.github/workflows/cty-archive.yml runs the same script on demand and publishes
the result twice: to GitHub Pages, and as a GitHub release. Run it with dry_run
first — that produces the artifact without publishing anything.
Pages is what a client should fetch from. The two addresses never change, and
each is a plain static file served with a single 200:
https://ok2cqr.github.io/cqrlog-cnty-files/ver.dat
https://ok2cqr.github.io/cqrlog-cnty-files/cqrlog-cty.tar.gzThat is deliberately the same shape as the ctyfiles/ directory CQRLOG has
always fetched from, so the download code needs no redirect handling. The
release assets carry the same bytes at
…/releases/latest/download/cqrlog-cty.tar.gz, but that address answers with two
redirects to a signed, expiring URL — fine for a browser or curl -L, not
something to hard-code into a simple HTTP client. Releases are there for history
and for the per-version notes.
Two deliberate departures from OK1RR's own archive: MASTER.SCP is the
SuperCheckPartial database rather than his hand-curated CW list, and
ContestName.tab is included, which upstream does not ship in the tarball.
Both are spelled out in packaging/cty/README, which travels inside the archive.
Status
The Pascal implementation is complete and tested but not yet wired into CQRLOG, which still runs its original engine. Known gaps, carried over deliberately:
- The US-state override is not ported;
us_states.tabships but is unread. Ambiguous.tblis loaded but not yet consumed by the resolver.data/dxcc/carries one local patch: two lines restoring Czech call areas between 1993-01-01 and 2005-04-30, which were unreachable in the upstream set. It belongs upstream with OK1RR, and until it gets there a data refresh will revert it — see data/README.md under Local patches, and docs/proposals/ for the reasoning.
Licence
The code is GPL v2 — see LICENSE.
The data in data/ is Martin OK1RR's work, redistributed as CQRLOG has
always redistributed it. It is not covered by the code licence. See
data/README.md.

