g | x | w | all
Bytes Lang Time Link
886JavaScript Node.js250306T111015ZArnauld
572C251007T074336Zanatolyg
1530Google Sheets250306T104648Zdoubleun
612Charcoal250307T134910Zuser1502
852Bubblegum250306T005540ZArnauld

JavaScript (Node.js), 886 bytes

Expects the dictionary linked in the challenge as an array of strings. Returns a matrix of characters.

This is the output used in the 856-byte version of my Bubblegum answer.(1)

d=>"T6+zkmN6ROU8FYlEJ8SaOBPhJdBJyFDXsWXUI16DD9AaLCPyEjvBxdbMxDD9JSfAc9BTuExWNPaE2hjVAA0ALF0ElqSxbPimJIPBruFz6CFvHjAK0kCFgNh7HtIB1mGluBX7EwVT7xD3nKxcBUcATdF+TEIqCYuAkBCB+KH4CsPCpDGZeG9xCgEBfsDCuBW+BmFH3xJJxVU/L0fG36CSKFC7edcE1FBZVJn0HnfHr4BbTDiFIW/AMaCmpEJpa0qLBkG7mDJTAfQB5rAlaFRMAXzW2NL5cCz0PRZGmZPEwECDJR9EaAGdBQESE7FAkOLVSI7qEjKN5fDVFHvqBObEonBb9DlaEdWDJyGexDl5DGPB8xFRvBgiEHYJEIAzeOd2DncGq9LfYFgfF0PbH+PqOZvJTL2BEqEPqGO2KCIGeDJcjBL3Pk0BW0JRhQ8CXc/BuiQ91LVJIUTNNRdLLRdsTunDnyMTEklgKdnUuJB/du63pn/IafhjZIrOFidBRORuWQ+0ooEYMZsLgTQCRLYTWfLnYkvquhtEI0mu2JDKv5AYLcg1eryg3x+icvDRyrvOZAvtCfB4GwyowaDPSqsCUZ7jFnvBaz2/Y2s4BV2ruHciHbq1K7SINjFMZ9nSi7cP9U".replace(/.../g,s=>[...d[(q=d[k=i&1]=Buffer(("BAAAAAADAABBAAAACABBAAAABBCFBEAVAACBCADBEFBICAFAMGp"[i++-163]||"A")+s,"base64").reduce((p,c)=>p<<8|c)-~d[k])/w/w|0]].map((c,j)=>(m[q%w+k*j]||=[..."".padEnd(w)])[q/w%w+!k*j|0]=c),m=i=[],w=45)&&m

Attempt This Online! (2)

(1) It has a slightly higher letter density than the 852-byte version but uses words that are as close as possible to the beginning of the dictionary, which is better suited for the compression method that I'm using here.

(2) In order to have a functional test link, unused words were replaced with empty strings in the dictionary.

Encoding format

We store a list of integers representing horizontal words, interleaved with a similar list for vertical words.

Each integer \$q\$ is interpreted as follows:

Each list is encoded as 24-bit delta values and each delta value is encoded as a 4-character string in base-64.

Because most words are taken at the beginning of the dictionary and because the lists are sorted, the 163 first base-64 strings all start with "A". This allows us to compress further by storing the leading characters separately.

Decoding example

Commented

d =>                    // d[] = dictionary
"T6+...P9U"             // primary lookup string
.replace(/.../g,        // for each 3-character substring
  s =>                  // loaded in s:
  [...d[                //   dictionary lookup:
    ( q =               //     set q
      d[k = i & 1] =    //     and d[k], where k = i mod 2
      Buffer(           //     by building a Buffer
        (               //     from a base64 string:
          "BA...Gp"[    //       secondary lookup string
            i++ - 163   //       read it at index i - 163
          ] || "A"      //       with default value 'A'
        ) + s,          //       prepend this to s
        "base64"        //       interpret as base64
      )                 //     end of Buffer()
      .reduce((p, c) => //     reduce it:
        p << 8 | c      //       using Big-Endian format
      ) - ~d[k]         //     add d[k] + 1 to this delta value
    ) / w / w | 0       //     integer division by w² (45²=2025)
  ]]                    //   end of dictionary lookup
  .map((c, j) =>        //   for each character c at index j
    (                   //   in the selected word:
      m[q % w + k * j]  //     get the relevant row
      ||= [             //     if it's still undefined,
        ..."".padEnd(w) //     set it to an array of w spaces
      ]                 //     (NB: we use k for the direction
    )[                  //     with 0=horizontal, 1=vertical)
      q / w % w +       //     get the relevant column
      !k * j | 0        //     (now guaranteed to exist)
    ] = c               //     write c at this position
  ),                    //   end of map()
  m = i = [],           //   start with m[] = [] and i zero-ish
  w = 45                //   w = grid width = 45 (*)
) && m                  // end of replace(); return m[]

(*) This for readability only. Hardcoding the values wherever needed would be just as long.

C, 572 bytes

#define R return F
#define O for
enum{j=4230,S=45,T};B,i,w,d,z,u,s;char A[j][5],U[j],E[j],*F,c;H(k,x,y,r){if(!k){if(d>63)R;++d;O(y=S;y--;)O(x=19;x--;){w=y*T+x;u=F[w-1]|F[w+5];s=0;O(i=5;i--;++w)s|=c=F[w],z=c>64?1:T,u|=F[w-z]|F[w+z];if(s>64&u<64&&H(4,x,y,0))R;}--d;}O(w=0;w<B;++w)if(!U[w]){int D[5],n=y*T+x,t=r?T:1;u=0;O(i=0;i<5;++i)D[i]=c=F[n],u|=c>>6&c!=A[w][i],F[n]=A[w][i],n+=t;U[w]=1;if(!u&&H(k-1,44-y-(k==3)*4,x,!r))R;O(i=5;i--;F[n]=D[i])n-=t;U[w]=0;}R-F;}main(){O(F=E+T;gets(E);B+=strlen(E)==5)memcpy(A[B],E,5);O(i=j;i--;)E[i]=i<j/2?i%T^S?32:10:0;puts(H(2,20,22,0));}

Requires C89 (for implicit int) and 32-bit environment (for implicit int/pointer type punning).

Command line:

m < 20k.txt

Output:

  alarm m t d b g c   event   young   style
    r a e h e r i r    i          allow
k c x j a i l i f o   users write m   italy f
o a i owner h notes    i     a    m   n w   r
r river s doing s s   store admin among apple
e d                   l      i     o    r   s
asses s c h c c m a s every hours entry death
 a  t h o e l l a place     i      t     r
 u  e o u l u o r p l pages never share front
 d  paper l basic l e  f    d     a      o
girls s tools e hayes state until needs brand
o                      e     o    t     e
i s t p p h v s i n f great press agent agree
n h o o a a i h r i o a      t     u    c
g i p s l p e a a g r music three times homes
  point a power q house     e      d     l
  s c study s print m score range deals ideal
                       h    m     a      e
s m r w p w t i i w m first space total brown
h o i h h o o n t o e  n     h    u  l
o v g e o r d d e u d based local maybe
which r nokia i multi o      t       u
s e these s years d about o could w image e t
       i       a      k hotel n again b money
      still south links e h i d v t d o a t p
       e  i    e     n  i e c e i e e v i e e
japan asian class yahoo r r k r d r x e l r s
   l      u     i    w
clips latex women using s black a board a i
   h     a      c     reply u issue r added
dream start while video e t d n k i e i o e q
    i    l     e      u e e i d e n a l b a u
metal plays leave cheap d s o s d g s y e s o
    e     t    e     p                      t
costs porno small there tells l staff o table
   r      r     e    i  i e heart l offer  e
basis party extra world t a a r o o u t a  g
   a     h      r     rules l g c n n e c  a
child login human miami e t l e k g d n kelly
a   e    n     d      v                   i o
final looks value price   s a parts p thing u
e   t a   h    l     h    cover o color n h t
s claim   o white order   o o i o a a a p t h
      poker          c    t i o m l n d u
  files   texas   weeks   t d r s e s eaten

The code uses search with backtracking. It turns out that considering only 5-letter words is enough! This simplifies management quite a lot.

The main searching function receives a partially filled matrix and tries to find a place to add a new horizontal word. When found, it puts a new word in 4 places, such that symmetry would be preserved.

Conceptually, searching for a place and searching for a word to put there are different functions which call each other recursively. But because of golfing I used one function for both. The return value is nonzero on success.

To start the search, I set the coordinate to the center of the matrix and set number of words to 2 instead of 4 - the center is the only place where 2 words are needed for symmetry.

For exit condition, I tracked the number of filled letters at first, but then found out that just tracking the recursion depth is enough. Needed density was achieved at depth 64.

I had to tweak the range of coordinates to avoid the first non-centered word intersecting with a rotated version of itself.


It took time to golf the code - it's definitely the largest golf code I have ever made! Here is a version before I started the really ugly transformations on the code:

#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <string>
#include <stdlib.h>
#include <string.h>

constexpr int LEN = 5;
char words5[9999][LEN]; // usable 5-letter words
int words5_size = 0;
bool used[9999] = {0}; // flag per word
constexpr int SIZE = 45;
constexpr int STRIDE = SIZE + 1;
char field_buf[(SIZE + 2) * STRIDE];
char* field = field_buf + STRIDE; // printable 45x45 field
int filled = 0; // counter of letters

void init_field()
{
    for (int x = 0; x < STRIDE; ++x)
        field[-STRIDE + x] = '\0';
    for (int y = 0; y < SIZE; ++y)
    {
        for (int x = 0; x < STRIDE; ++x)
            field[y * STRIDE + x] = ' ';
        field[y * STRIDE + SIZE] = '\n';
    }
    for (int x = 0; x < STRIDE; ++x)
        field[SIZE * STRIDE + x] = '\0';

    filled = 0;
    if (words5_size == 0)
    {
        std::ifstream file("20k.txt");
        for (std::string line; std::getline(file, line); )
            if (line.size() == 5)
                memcpy(words5[words5_size++], line.c_str(), LEN);
    }
    for (int i = 0; i < words5_size; ++i)
        used[i] = 0;
}

void debug_print()
{
    std::ofstream file("debug.txt");
    file << field << '\n' << "Filled: " << filled << '\n';
}

bool search(int its, int x, int y, int dir)
// its - if 1...4, defines the position in a group of 4 words
// its - if 0, it's a command to look for another group to place
// x, y - coordinate of word to place
// dir - 0=horizontal, 1=vertical
// returns success
{
    if (its == 0)
    {
        // Exit with success 
        if (filled > SIZE * SIZE / 2)
            return true;
        // Searching for a place to add 4 words
        for (y = SIZE; y--; )
        {
            // Tweaked starting point to avoid intersection of a word with its rotated version
            for (x = SIZE / 2 - 3; x--; )
            {
                int index = y * STRIDE + x;

                // checks for intersection between words
                char cross = 0;

                // checks for bad interactions between words:
                // touching end-to-end or running parallel and adjsacent
                char usability = field[index - 1] | field[index + LEN];

                for (int i = LEN; i--; ++index)
                {
                    char c = field[index];
                    cross |= c;
                    int side = c > ' ' ? 1 : STRIDE;
                    usability |= field[index - side] | field[index + side];
                }
                // If allowed, search deeper
                if (cross >= 0x40 && usability < 0x40 && search(4, x, y, 0))
                    return true;
            }
        }
        return false; // reached only if the whole search fails
    }
    else
    {
        // Starting search from index 2 - works faster by luck
        for (int w = 2; w < words5_size; ++w)
        {
            if (used[w])
                continue;
            int undo[LEN];
            int was_filled = filled;
            int fill_not_ok = 0; // checks if all intersections have correct letters

            int index = y * STRIDE + x;
            int next = dir == 0 ? 1 : STRIDE;

            // Place the word in the field, remembering what was there before
            for (int i = 0; i < LEN; ++i)
            {
                char c = field[index];
                undo[i] = c;
                filled += c <= ' ';
                fill_not_ok |= c >> 6 & c != words5[w][i];
                field[index] = words5[w][i];
                index += next;
            }
            used[w] = true;

            // If allowed, search deeper
            if (!fill_not_ok &&
                search(its - 1, SIZE - 1 - y - (its == 3 ? LEN - 1 : 0), x, !dir))
            {
                return true;
            }

            // Search failed - undo the placement
            for (int i = LEN; i--; )
            {
                index -= next;
                field[index] = undo[i];
            }
            used[w] = false;

            filled = was_filled;
        }
        return false;
    }
}

int main()
{
    init_field();

    // Start the search from 2 words in the center
    search(2, SIZE / 2 - LEN / 2, SIZE / 2, 0);
    debug_print();
    return 0;
}

(a little tweak in the code - starting search from the third word instead of first - makes it finish faster; I think it would work without the tweak, but the C++ version is too slow)

Golfing tricks:

Google Sheets, 1530 1965 bytes

=let(i,importdata("https://gist.github.com/eyturner/3d56f6a194f411af9f29df4c9d4a4e6e/raw/63b6dbaf2719392cb2c55eb07a6b1d4e758cc16d/20k.txt")
,map(tocol(split(reduce("ID#CU)184*BH$p#t;#a+e#274(o#DB(QE;B6#G5&u%g%n(e#T7$o#a;#k)a#d$12I#e$OA$l(u$t#v;#e(DV#i#f%o%c%5O7&g$o#e;22L#a#e#d%l#13W#AP$t#Q1$s#l;c#c$EV#DE%g'n#18F%a(;h#t$n#e#t$w$e'e$G0&R5$a#f;o#i$a#n&i$KY$m%e#t&r%d#a#n#u;o#o$d#d#1WT)OK$75#CG#t;l#n#1U7$c#S0%d%t'r#t$n#w#o#u;&a#i$5FF(2W0#FD#a%259;#77N$t#o$KK%l$r(PS#y#e#e;#i$i#h$l#m$l*s#33$t#e$f#s$b#;#d$n#i$e#m#13)g%EH$f$t#r#;#e#RD$o#h#d$11E#UA#o#e#QV#a#;#o#c#d#a%n#o#v$n)n%e#r#c#r#c$a#n#;#s#r#5I5$u#i$t#5W&l#u#e#a#1SK#;%o#t#l#a#YC#a(9P#m#n#n$p#k#s#;7W3#e#t#o#d#e#47$p%o$s#t#c$d#s%;r$s#r#d#i#i'a&e$o#a#p(e$a&c;i)137#l#t&c$r#u#i%s%PP#e;d&a%n#t%o#i#1V#t#c%a#a%e&n;a#WI%s%n#n#e$i&h#s#5V7)t;y&i$b(d#g#n$o&o'e#u#r#s#a$r;%i#m$e#b#g$o%t$3Q#p#p#t#m#e#3BS;#i#n#a$c#u#l#26(s#78J#n#n#u#o%;#1MI#o#r#o#f&4G#t$a#l$HU#u#s#;#p#i$a#m#e#b#f%u)a$y#i#b%a#i#n#o#;#a#SC#a#a#KZ#NX$e#c#r$X0#c#;#c#e$r$48%l)HX#o#c$o#p$i#;#t$d#i$l#o$LP#a*r$w#u$b#o$a#;a#f#u#UJ(s$p%ZA$s#s$1K7#;5OJ%a#L1#P7(6D9$l#t&;r#r#i#c$s#e'b%r%2DU#o$IW#s#s;i#J2#MK$ZR)3K#e#e$a#o;c#a#g#u%n&e#a%x$VM$p&a#t$f#u;a#t$789&13S$p'i$e$s#r#u$a#r;(c%C1#d'r%PG#D3$r#c;a#f$94#e$DQ#L0#d%o#r#s#IF;c#a$o&6F%l%t%u#o#NE(o#;c#m$u(u$PN$t#168$j#e)u#;e#i$R4#l(i%y%t&E4#ES;BX(DZ#o(1TJ#c+s#;s#y$QL*V3)EM#6H"
,sort(row(1:9),1,),lambda(a,i,substitute(a,char(34+i),rept(",",i)))),";"))
,lambda(r,let(t,split(r,",",,),join(" ",map(t,lambda(k,ifs(isblank(k),,len(k)=1,k,1,index(filter(i,len(i)>1),decimal(k,36))))))))))

(added three newlines for readability)

Uses a hard-coded map of the first matrix output by Arnauld's generator, unpacks run length encoded token separators, and untokenizes by indexing, in base 36, the dictionary at the raw link.

screenshot

Ungolfed:

=let(
  import, importdata("https://gist.github.com/eyturner/3d56f6a194f411af9f29df4c9d4a4e6e/raw/63b6dbaf2719392cb2c55eb07a6b1d4e758cc16d/20k.txt"),
  dictionary, filter(import, len(import) > 1),
  packed, D2,
  tokenized, reduce(packed, sequence(9, 1, 9, -1), lambda(a, i,
    substitute(a, char(34 + i), rept(",", i))
  )),
  tokenRows, tocol(split(tokenized, ";")),
  map(tokenRows, lambda(tokenRow, let(
    tokens, split(tokenRow, ",", false, false),
    join(" ", map(tokens, lambda(token,
      ifs(
        isblank(token), "",
        len(token) = 1, token,
        true, chooserows(dictionary, decimal(token, 36))
      )
    )))
  )))
)

...where cell D2 contains tokens created with this packing code:

=let(
  import, importdata("https://gist.github.com/eyturner/3d56f6a194f411af9f29df4c9d4a4e6e/raw/63b6dbaf2719392cb2c55eb07a6b1d4e758cc16d/20k.txt"),
  dictionary, filter(import, len(import) > 1),
  tokenRows, map(tocol(A1:A, 1), lambda(row, let(
    words, split(row, " ", true, false),
    tokens, arrayformula(
      if(len(words),
        ifna(
          base(match(words, dictionary, 0), 36, 2),
          words
        ),
        iferror(ø)
      )
    ),
    join(",", tokens)
  ))),
  reduce(join(";", tokenRows), sequence(9, 1, 9, -1), lambda(a, i,
    substitute(a, rept(",", i), char(34 + i))
  ))
)

...where column A1:A contains the matrix to tokenize, one string per row. In each row, text is split at spaces. One-letter words tokenize into themselves and longer words tokenize into base 36. One-letter words aren't tokenized because the dictionary doesn't contain all one-letter words. Token separators are RLE coded.

Charcoal, 676 633 612 bytes

Found by taking one of Arnauld's crosswords and locally optimizing it for compressibility, keeping the pattern of spaces fixed.

Crossword:

object report       models        passed  t s
 r         e modern      a enable      except
server manage    e   l   h      n images  s d
 a       e i  budget e  camera  d      i  t e
 t      second n a   s   r   suburb    r  e r
thanks t d n   l rights causes  r escape  d r
a r  opened spread   e     e itself   r      
s c  n r d  n  s     r  styles    obtain  s e
t a  a e    a  search  i   l t    r   s e t s
e d  t s laptop       estate  linked dollar s
s e hawaii  c entire   s   r     e a  n d a a
    a a  techno      sounds extent l   verify
 driver  t e  placed   e  t      toledo r n s
 a  i r  e l  l        r parent  l a  r s  g 
 e  n e  r i legacy       t   orders  g  d r 
 m agenda  n e e  avatar number i e grande e 
 o r n i   e n n  n       e   r n s a s  p e 
 n c speech  d r  k define    e e a r martin 
   h u s e themes e      assets r l n  n h e 
career e n e r s census  t   e  s e e  a s   
h  r e l t n     r    c  i a n      t  l    s
a       around u o    a  n s a   l   quotes a
s    c   e r   n s bearing s n   e s   g    t
e layout   e   e s e  l    e t detail       i
r    r  p      s e r  e    s     r s a t s  r
   a n  a n i  c   l  topics e a a t e active
 t r e  t a s polski      r exists e r t r   
 errors e s a a    nature e  e h  artist a s 
 s e  e n s b l   s       a  t l p   a o i h 
 t select a e appear direct  e e l  almost e 
 e t  i  public   u       energy e s  o w  e 
 r  l n  e a  effect s        r  a e  r e  t 
e e i guitar      e  a   studio  s c  agents 
losing   e resize submit      urgent  l t    
a t e t  r i     t   s   signup e  offers r s
i houses sweden  option       senior l e  e t
n e p r   a    t n   n  filter  t    e m  s e
e r  travel    render  a     e  l  s e i  c e
      o   turkey e     t   device beacon  u l
r s  bridge w  issues choose e   a r e delete
e c  o    reason   l   e   e a russia      h 
s h  s      n  google  n supply  p e       r 
i e  skills d      i   s   e    wilson person
damage      amazon o      orange r         n 
e a  single        titles       leaves padres

Charcoal Code:

”}|⊟ÀGiπ~ü⊗⪪⁺P¹∕U286×´ρ9#E⸿⁴\u≔¡μ.₂ab▷⸿↶ξ↓@q3Gsd₂↥‴9/[⁶3↷⭆Rh℅ερ²u-cVⅉ~^↶⁸⁷ê;ω◧⊕▶◨⊖]?oβPθ¶@3ς²Om∕ⅉθ№÷Lξ[№:D⟲W⁴oψ⸿We…₂⸿θe|¶I³↧\FκN﹪²o~✳dι0¤pAηR×+℅‽%TY?z³1μw∕‽⊖ⅉ∕E-5▷×‖J7³¦w≡›PCT∧fπ∕↔σ,⁼B%‴~À:W⁰λ'~#¹⁵>P⟦<⌊¬σ⁹⁻rξ⭆´@∧cC✳ZN↶�α↥^b↶←ψδ⮌RRJE⊟⊘H↔¤Zν↓Z¿¿d¹↔³eCc⁹⟧c⁰^ψ;ⅈ№K⌕υ⁶⭆um(T»◨χw↧Rx¬⁶↘αhC⁷≦⊟IK∧⭆‴]]…R⌕↙VSⅈ*O⎚﹪|F÷(l⁶⊖⊞(Þrb2x%↧PR⪫Y﹪θQ#ν⁷u⧴zr▷‖⁵|Ybl´↧⬤9π⟧Π↷Þj$▶№φ⊖¶^“ΠgPκ″K⁸⎚+⁸⧴ι&.⦄↥)^⬤CυoνR²L'›↖O№γ:z G“ξ~⁵⍘¡^₂4bB3WK∕p✂3iÞψgς‴d:G�}➙`Ed³~Uηê≦⁺i:«S}τ¬Q⊕f↘¬rt4“≔›_EURw⌕Φ}«¬N″⊙ζMhJ:ντaRε⁸O∕?¬¤WS-V»Yj¹∕|⊗M⪫f*X÷zΠdⅈ∕±ξ5¡DXNTT8G↔M⁷→ⅉX⸿…¤CΣM⎇cV¡"b⁶⁻HSπμ⎇⟲,X⊕↧W₂yφA2{lIFκ▶~➙↨ü0▷K.ÞT⊖gX⊘¹↷N)L[ΦQ↙4«↷⁰»∧αγ\⁼▶⦃P⁼duM;λ"⎚AYL÷r»⁺ξωξ⧴λ≕

Bubblegum, 852 bytes

Thanks to @ovs for suggesting switching from LZMA to DEFLATE with Zopfli.

1013 letters in the grid.

00000000: 25d3 d7c1 e328 1000 e077 15f3 f584 c458  %....(...w.....X
00000010: e68c c007 c8ff 7a9b bf48 6672 ac65 ad18  ......z..Hfr.e..
00000020: 52dd ef6b 3a7a 1a33 0020 f32a 7ffc 3335  R..k:z.3. .*..35
00000030: ef0d 0590 8003 fcf4 51f3 e406 6e42 dd7e  ........Q...nB.~
00000040: 4659 31f1 a711 57ff 843e f6b2 7e83 9be1  FY1...W..>..~...
00000050: 196d a5c2 9436 54ca 9c77 0ce0 0df6 9266  .m...6T..w.....f
00000060: 6412 70f4 b9ea 779b 049e 427f 3cca 11c2  d.p...w...B.<...
00000070: 01d0 dfd1 4a86 20c4 b652 cb71 29c2 8f81  ....J. ..R.q)...
00000080: 344b 6ad3 88d8 fb8b 533f 8e7b 4cb2 b125  4Kj.....S?.{L..%
00000090: 8974 4669 a73d daec 8d1d e07e affe d310  .tFi.=.....~....
000000a0: 246c a745 554d 4c9c 69d4 7200 7492 57d4  $l.EUML.i.r.t.W.
000000b0: 1a83 b985 4291 4143 25f8 49e5 1383 1f16  ....B.AC%.I.....
000000c0: 128b f796 7576 b43e 566f 0493 0261 a5f2  ....uv.>Vo...a..
000000d0: 8e62 ae5e a349 1bcd 7e8f 4837 a4fd 9e91  .b.^.I..~.H7....
000000e0: 514b 7b45 6648 32c1 606c 100a 576f 2526  QK{EfH2.`l..Wo%&
000000f0: 4086 67af ffc7 8bd7 fe5d 31b7 a3b7 35ca  @.g......]1...5.
00000100: ce2e 98f7 75c5 5004 24a6 d049 4211 1b9c  ....u.P.$..IB...
00000110: f651 e251 bf12 d00c 33de ab1c 546b c4b5  .Q.Q....3...Tk..
00000120: 5793 0d42 6798 f23d 4a3b c900 cffb 4a6d  W..Bg..=J;....Jm
00000130: 128a 696d 3e31 5a6f d0dd 9ea4 3c4a 6ace  ..im>1Zo....<Jj.
00000140: b84a 2b42 47f5 dbed b9e9 42e3 48ad f5a6  .J+BG.....B.H...
00000150: d338 010e c340 11c2 d80c 8742 08b2 4cb6  .8...@.....B..L.
00000160: fa4f 8c09 2fd5 95fe d385 be59 42a7 1a84  .O../......YB...
00000170: e009 c4af 23e6 5424 3e4a bfe2 4c9b d034  ....#.T$>J..L..4
00000180: aa0f 619a a596 23d1 d069 1add 41b3 36bf  ..a...#..i..A.6.
00000190: 2c93 2480 8046 91dc e048 e5e8 5366 6307  ,.$..F...H..Sfc.
000001a0: e39e b324 bb80 2fdd 4c2d a70a 43e5 66a3  ...$../.L-..C.f.
000001b0: f0f6 0084 971a 693d 63f8 ca80 100c 3626  ......i=c.....6&
000001c0: 53c0 abb4 3443 50a0 9ad6 4867 3940 b0b1  S...4CP...Hg9@..
000001d0: bca2 b5a8 a019 3e5c 7003 e1c9 ae78 d892  ......>\p....x..
000001e0: c1ed 60f7 a020 88ab b4b8 dc4e 9a44 d724  ..`.. .....N.D.$
000001f0: dbac fd27 b2e0 56ec d1ce 5481 4c73 5314  ...'..V...T.LsS.
00000200: 9ae9 b2bd a17f e78a 2104 ccd2 ce1a 0aa1  ........!.......
00000210: 706b 4c49 b115 1f53 c2e9 ff1b 2476 e6ff  pkLI...S....$v..
00000220: cd34 499a 6a1b ba3f bd10 969c 3e25 a6f9  .4I.j..?....>%..
00000230: bdf6 5e39 340d ae32 637c 6da1 bb15 ce11  ..^94..2c|m.....
00000240: 6906 c04e dca3 bf93 aa73 5bb0 91a5 71a5  i..N.....s[...q.
00000250: 5614 725f 2bb2 5d06 86dc 6b4d 63fa c296  V.r_+.]...kMc...
00000260: 7453 a2a9 4e02 42f2 47ef 574d 1c32 b5d4  tS..N.B.G.WM.2..
00000270: 5a52 dbd6 3fea 9e5f 9ce2 4823 4f48 00df  ZR..?.._..H#OH..
00000280: d45e 1114 015b e766 11c2 6f56 2f35 1672  .^...[.f..oV/5.r
00000290: 1c23 0266 c415 d99b ed52 5aee 7d18 ff8a  .#.f.....RZ.}...
000002a0: c902 8241 62c4 2c73 e141 35b7 42a1 22b3  ...Ab.,s.A5.B.".
000002b0: c831 cbd9 4834 34b0 7b90 a4ed 6046 8b23  .1..H44.{...`F.#
000002c0: b959 00b9 5fa5 750c 9c42 b03c 373c c9a8  .Y.._.u..B.<7<..
000002d0: e513 f50b 3079 3ffb eacd 99a2 9683 90b6  ....0y?.........
000002e0: 24f1 e8e3 ec4b 505a 4d2d fb89 78d5 2faa  $....KPZM-..x./.
000002f0: 53f5 b2a2 e518 db90 a930 d728 9f00 f84a  S........0.(...J
00000300: ae54 eaea 9282 42da 7a2d 9f92 8042 bdaf  .T....B.z-...B..
00000310: 3d06 18c0 de7f 9576 d2b0 ddde 7c62 94c7  =......v....|b..
00000320: d76f 3278 f7f9 0f85 771a 4754 5fcc 67c4  .o2x....w.GT_.g.
00000330: 9a5b 1374 a033 7bcd 3140 0001 58d8 b25f  .[.t.3{.1@..X.._
00000340: eeb1 a796 6800 f018 69c6 30d7 e8af 30d7  ....h...i.0...0.
00000350: b7de f36f                                ...o

Try it online!


Bubblegum, 856 bytes

1029 letters in the grid.

Try it online!


Generator

The 856-byte version is the first solution found by this ugly piece of code:

(Don't) try it online!

Basically, it starts by placing a cross of 2 words of odd length in the center of the grid, then attempts to add connected words, 4 at each step with the required 4-fold rotational symmetry, until 50% of the grid is filled with letters.

The 852-byte version was obtained by making the same algorithm run for a little longer and periodically shuffling the dictionary to get completely different grids. (It has been tested with LZMA though, so it is unlikely to be such a good choice for Zopfli which is now used.)