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.

- For edges — Catan “roads” — allocate a logical array of (H+1)×(W+1)×3
- For intersections — Catan “settlements” — allocate a logical array of (H+1)×(W+1)×2
- 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

- 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.