At this point, I'm sure you're already familiar with the basic "hex grid" data structure:

The q-axis is 0deg→︎; the r-axis is 120deg↙︎.
┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐
┌─┘   └─┬─┘   └─┬─┘   └─┬─┘   └─┬─┘   └─┬─
│ (0,0) │ (1,0) │ (2,0) │ (3,0) │ (4,0) │
┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─
┌─┘   └─┬─┘   └─┬─┘   └─┬─┘   └─┬─┘   └─┬─┘
│ (0,1) │ (1,1) │ (2,1) │ (3,1) │ (4,1) │
┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─
┌─┘   └─┬─┘   └─┬─┘   └─┬─┘   └─┬─┘   └─┬─┘
│ (0,2) │ (1,2) │ (2,2) │ (3,2) │ (4,2) │
┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─
┌─┘   └─┬─┘   └─┬─┘   └─┬─┘   └─┬─┘   └─┬─┘
│ (0,3) │ (1,3) │ (2,3) │ (3,3) │ (4,3) │
─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─
└─┬─┘   └─┬─┘   └─┬─┘   └─┬─┘   └─┬─┘



Here's a simple way to implement edges and intersections, for example if you're making a bootleg of Catan:

Say that your logical array for the tiles has a geometry of H×W; this array will of course be sparse if you have a board that's any shape other than a parallelogram.

1. For edges — Catan “roads” — allocate a logical array of (H+1)×(W+1)×3
2. For intersections — Catan “settlements” — allocate a logical array of (H+1)×(W+1)×2
3. Define the intersections “adjacent” to tile (q,r) to be {(q+1,r,2), (q+1,r+1,1), (q,r+1,0), (q,r,2), (q,r,1), (q,r,0)}
• In other words, intersection 0 is due-north of its quasi-corresponding tile, and intersection 1 is north-west of it
4. Define the edges “adjacent” to intersection (q,r,0) to be {(q,r,0), (q,r,1), (q,r-1,2)}; and define the edges “adjacent” to intersection (q,r,1) to be {(q,r,2), (q-1,r-1,0), (q-1)}
• In other words, edge 0 is north-east of its quasi-corresponding tile, edge 1 is north-west of it, and edge 2 is due-west of it.

You could have chosen any arbitrary 2 intersections and 3 edges for each tile, but choosing them this way is optimal for tile-first applications. If a tile exists, all adjacent edges and intersections are guaranteed to exist; and for a non-sparse board, there are no wasted intersection or edge allocations.

Warning: This site uses Akismet to filter spam. Until or unless I can find a suitable replacement anti-spam solution, this means that (per their indemnification document) all commenters' IP addresses will be sent to Automattic, Inc., who may choose to share such with 3rd parties.
If this is unacceptable to you, I highly recommend using an anonymous proxy or public Wi-Fi connection when commenting.