Freeform Strategy

Adjacency-Aware

Two-phase: places your words with parallel adjacencies, then fills perpendicular gaps with real dictionary words.

Standard freeform crossword placement treats the problem as one of finding shared letters between words: two words can cross if they share a letter at compatible positions. The greedy algorithm picks the best crossing it finds and moves on. This produces grids where words intersect but rarely touch side-by-side, since parallel adjacency creates 2-letter perpendicular fragments that cannot be validated as complete words in a single pass.

The Adjacency-Aware strategy addresses this with a two-phase approach. In Phase 1, words are placed using both perpendicular intersections and parallel adjacencies. Parallel placements create 2-letter column pairs that are provisionally accepted if the pair is an attested bigram (consecutive letter pair appearing in at least 0.1% of dictionary words). Phase 2 then scans for those 2-letter fragments and attempts to extend each into a real dictionary word of length 3 to 7.

This two-phase design resolves a chicken-and-egg problem inherent to parallel placement. Two words placed side-by-side always create perpendicular columns of exactly 2 letters, which are too short to validate as real words during placement. Rather than rejecting all such placements (which eliminates parallel adjacency entirely) or accepting them unchecked (which produces gibberish), the algorithm defers final validation to Phase 2, where dictionary words can bridge the gaps.

The resulting grids contain both the user's original words and a set of dictionary fill words. The fill count is capped at 75% of user word count to prevent dictionary-dominated layouts. Because the grid contains more total words than other strategies, its raw intersection count is higher, though some portion of that increase is attributable to the additional fill words rather than more efficient placement of the original words alone.

Walk through a placement

Step through the algorithm's most distinctive move: fitting two words side-by-side when they share no letters.

1. Place the anchor word

1 / 6
C
O
R
A
L

Start by dropping the first word into the grid. CORAL is our anchor; every subsequent placement has to attach to it (or to a word that attaches to it).

Words: CORAL, FLAME

Strengths

  • Produces grids with substantially more intersections, though the increase is partly due to additional fill words from the dictionary
  • Fill words are drawn from a curated crossword dictionary and are validated against perpendicular constraints
  • Grids exhibit the interlocking aesthetic of newspaper-style crosswords, where words touch side-by-side as well as crossing

Weaknesses

  • !Significantly higher compute cost (5x to 50x slower than other strategies depending on word count), driven by dictionary lookup, parallel candidate enumeration, and Phase 2 gap filling
  • !Introduces dictionary words not from the user's list; the grid is no longer composed exclusively of user-chosen words
  • !Fill quality depends on dictionary coverage and bigram filtering; unusual letter combinations near the grid boundary may not find a valid fill word, leaving some 2-letter fragments unresolved

How It Works

  1. 1

    Build validation sets

    Combine the user's word list with the built-in crossword dictionary (approximately 42K entries, filtered to words of 3 to 8 letters with a quality score of at least 60). Build two data structures: a valid-word set for verifying that 3+ letter perpendicular runs are real words, and a frequency-filtered bigram set for provisionally accepting 2-letter perpendicular pairs during Phase 1. The bigram frequency threshold is max(3, 0.1% of combined word count).

  2. 2

    Phase 1: Place user words with parallel adjacency

    Order words by graph connectivity (same ordering as Densest Crossings). For each word, enumerate both intersection placements (perpendicular to existing words at shared-letter positions) and parallel placements (same direction as an existing word, offset by one row or column). Validate each candidate: 2-letter perpendicular runs must appear in the bigram set; 3+ letter runs must be complete words in the valid-word set. Score candidates by intersections (weight 10), valid 3+ letter perpendicular words formed (weight 8), fillable 2-letter gaps (weight 2), and open anchor cells (weight 1). Place the highest-scoring valid candidate.

  3. 3

    Phase 2: Fill perpendicular gaps from dictionary

    Scan the grid for all maximal contiguous letter runs of exactly 2 letters that are not already covered by a placed word in that direction. For each gap, search the dictionary for a word of length 3 to 7 that passes through those existing letters at some alignment offset. The candidate must satisfy the same perpendicular validation as Phase 1. Place the first valid match found. This pass runs once with no cascading (newly placed fill words do not trigger additional gap scans). Total fill words are capped at max(3, 75% of user word count).

  4. 4

    Score and rank results

    Across multiple randomized attempts (default 16), collect distinct layouts (deduplicated by word-position signatures) and rank by total intersections, then total words placed, then compactness (grid area divided by placed words). The top layouts are returned.

Pseudocode

// === Phase 1: Place user words ===
validWords = buildSet(userWords ∪ dictionary)
bigrams = buildFrequentBigrams(userWords ∪ dictionary)
// threshold = max(3, floor(|combined| × 0.001))

function isValid(word, row, col, dir, cells):
    for each empty cell with perpendicular neighbors:
        run = walkFullPerpendicularRun(cell)
        if len(run) == 2 and run not in bigrams: return false
        if len(run) >= 3 and run not in validWords: return false
    return true

function score(word, row, col, dir, cells, dict):
    for each 2-letter perp run:
        if canExtendToWord(run, dict): fillableGaps++
        else: unfillableGaps++
    return intersections × 10 + validPerpWords × 8
         + fillableGaps × gw - unfillableGaps × gw × 2
         + openAnchors × 1

for word in graphGuidedOrder(words):
    candidates = intersectionPlacements ∪ parallelPlacements
    best = candidates.filter(isValid).maxBy(score)
    if best: place(word, best)

// === Phase 2: Fill gaps with dictionary ===
gaps = findShortRuns(grid, placed)  // 2-letter fragments only
maxFill = max(3, floor(|placed| × 0.75))
for gap in gaps (until maxFill reached):
    for wordLen in 3..7:
        for alignment of gap within word:
            match = dictionaryLookup(constraints)
            if match and isValid(match):
                place(match, isFill=true); break

Complexity

Phase 1: O(W² · L³ · D) for candidate enumeration, where D is the dictionary lookup cost per candidate cell. Each cell in a parallel placement candidate checks whether its 2-letter perpendicular run can be extended into a real dictionary word (length 3 to 7), requiring a scan of the relevant dictionary bucket. Inline fill adds O(G · 5 · D_L) per parallel placement, where G is the gap count, 5 is the word-length range tried, and D_L is the average bucket size (~7K words). Total observed: 126ms for 9 words, 782ms for 40 words, 5.1s for 156 words (measured in single-threaded JS, no Web Workers).

Benchmark Results

Measured locally on three wordlist scenarios. Each cell is averaged across 3 runs of 16 attempts each. Best= top layout's metric. Compactness = grid area per placed word (lower is denser).

ScenarioBest ×Avg ×PlacedCompactness2-Ltr FragsFill WordsTime
Themed (9 words)
Short personal wordlist with low letter overlap, typical of user input.
161512/9603125.9ms
Vocabulary (40 words)
Common 5-letter English words with high letter overlap.
9990.2569/407.711029781.9ms
Mixed (156 words)
Mixed-length English words (3–8 letters), broad coverage.
312300.5228/1567.0135725129.8ms

★ Best across all four strategies on this scenario. Reproducible via scripts/benchmark-strategies.ts.

Metric Definitions

Each benchmark run executes 16 randomized attempts of the strategy and returns up to 4 distinct layouts, sorted by quality. This process is repeated 3 times to reduce variance. The metrics below describe how each column is derived from those runs.

Best ×
The highest intersection count from the top-ranked layout across 3 independent runs. An intersection is a grid cell shared by two words (one across, one down). Higher is better.
Avg ×
The mean intersection count across all returned layouts (up to 4 per run), averaged over 3 runs. This includes second- through fourth-best layouts, so it reflects consistency rather than peak performance.
Placed
The most words placed on the grid in any top-ranked layout. For Adjacency-Aware, this includes dictionary fill words from Phase 2, so it can exceed the input word count. Shown as placed/input.
Compactness
Grid bounding-box area (rows times columns) divided by the number of placed words. Lower values indicate denser packing. Note that this measures the full bounding box, which includes empty cells between words. Adjacency-Aware's denominator is inflated by fill words.
2-Letter Frags
The number of 2-letter contiguous letter runs in the best grid that are not covered by any placed word in that direction. These are perpendicular fragments left behind by parallel placements that Phase 2 could not extend into complete dictionary words. Only applicable to adjacency strategies; always 0 for other strategies since they never place words in parallel.
Fill Words
The number of dictionary words added in Phase 2 to extend 2-letter gaps into complete words. These are not from the user's word list. Capped at max(3, 75% of user word count). Only applicable to adjacency strategies.
Time
Wall-clock time for one run (16 randomized attempts), averaged across 3 runs. Measured in single-threaded JavaScript without Web Workers. Includes ordering, candidate enumeration, validation, and (for Adjacency-Aware) Phase 2 gap filling.

Comparative Performance

All four strategies measured side-by-side on the same word lists and hardware. Rows highlighted in blue indicate the strategy being viewed.

Themed (9 words)Short personal wordlist with low letter overlap, typical of user input.

StrategyBest ×Avg ×PlacedCompactFragsFillTime
Adjacency-Aware161512/9603125.9ms
Parallel-Seeded2018.7514/9525128.8ms
Densest Crossings98.259/911.56001.3ms
Longest First889/98.56001.2ms
Balanced98.259/915.89001.4ms

Vocabulary (40 words)Common 5-letter English words with high letter overlap.

StrategyBest ×Avg ×PlacedCompactFragsFillTime
Adjacency-Aware9990.2569/407.711029781.9ms
Parallel-Seeded9693.568/4011.031128901.6ms
Densest Crossings4140.540/4019.20017.6ms
Longest First4240.2540/4015.950014.8ms
Balanced4140.540/4015.60019.6ms

Mixed (156 words)Mixed-length English words (3–8 letters), broad coverage.

StrategyBest ×Avg ×PlacedCompactFragsFillTime
Adjacency-Aware312300.5228/1567.0135725129.8ms
Parallel-Seeded306292.75228/1568.624725230.4ms
Densest Crossings157156.25153/1561000317.6ms
Longest First158153.25151/15610.330078.6ms
Balanced161157.25156/15612.3800306.8ms

Analysis

The four strategies share the same underlying greedy placement algorithm but differ in word ordering and, in the case of Adjacency-Aware, in which placements are considered valid. This means performance differences stem from two factors: the cost of computing the ordering, and the size of the candidate set evaluated per word.

Graph-guided, Balanced, and Longest First operate exclusively on the user's word list. Their placement pass considers only perpendicular intersection candidates (positions where a new word crosses an existing word at a shared letter). Longest First skips the graph-build step entirely, making it the fastest strategy in every scenario. Graph-guided and Balanced are similar in cost because they both compute the compatibility graph; Balanced adds a minor constant factor for its hybrid seed selection.

Adjacency-Aware adds two sources of overhead. First, each word's candidate set is larger because parallel placements (same direction, offset by one row or column) are enumerated alongside perpendicular intersections. For a word of length L placed beside an existing word of length P, this adds up to 2 × (L + P - 1) additional candidates per pair. Second, every candidate undergoes perpendicular-run validation against the dictionary, which requires walking the grid in the perpendicular direction at each cell. Phase 2 (gap filling) adds a third cost: scanning for 2-letter fragments and searching dictionary buckets for valid extensions.

The practical impact scales with word count. On a 9-word themed list, the adjacency strategies run in roughly 126-129ms versus 1.2ms for Longest First (approximately 100x slower). On 156 words, the gap narrows to roughly 65x (5.1-5.2s versus 79ms). The overhead comes from dictionary validation of perpendicular runs, parallel candidate enumeration, and gap filling.

An important caveat applies when comparing intersection counts across strategy types. The adjacency strategies' reported intersections include crossings involving dictionary fill words. For the mixed-large scenario, Adjacency-Aware places 228 total words (156 user + 72 fill) and reports 312 intersections, while Densest Crossings places 153 user words and reports 157 intersections. Some portion of the intersection difference is attributable to the additional fill words, not to more efficient placement of the original word list.

The 2-letter fragment count reveals an inherent tradeoff of parallel placement. Every pair of words placed side-by-side creates perpendicular 2-letter runs that must either be extended into dictionary words or left as fragments. Inline gap filling (used by both adjacency strategies) mitigates this by resolving gaps immediately after each parallel placement, while surrounding constraints are still loose. Parallel-Seeded reduces fragments by roughly 43% compared to deferred filling on large word lists, producing 24 fragments versus 35 for base Adjacency-Aware on the mixed-large scenario. The remaining fragments are structurally unfillable due to constraints accumulated during grid construction.

The compactness metric (grid area divided by placed words) also requires careful interpretation. Adjacency-Aware tends to produce grids with lower compactness values, which suggests denser packing. However, the fill words extend existing perpendicular runs rather than expanding the grid boundary, so they reduce compactness partly by inflating the denominator without proportionally increasing the numerator. Among the user-word-only strategies, Longest First tends to produce the most compact grids, likely because long anchor words create a tighter bounding box relative to the words they accommodate.

Each strategy was run 3 times with 16 randomized attempts per run. Adjacency-aware loads the full crossword dictionary (~42K words, filtered to 3-8 letters with score >= 60) for perpendicular validation and gap filling; other strategies use only the scenario's word list. Note that adjacency-aware's "placed" count includes dictionary fill words from Phase 2, so its intersection counts are not directly comparable to other strategies, which only place words from the input list.

When to Use This

  • You want a dense, interlocking grid and are willing to accept dictionary fill words alongside your own word list
  • You prefer the newspaper-style crossword aesthetic where words touch side-by-side, not just at perpendicular crossings
  • Your word list has enough common letters that parallel placements can form plausible bigrams for Phase 2 to resolve

Try it

Open the creator with this strategy pre-selected.

Try Adjacency-Aware

Other Strategies