[SPOILERS] I can share some tips right now. Let competition *and* collaboration raise the bar. Darwin and Kropotkin unite.
Who am I to talk? Well, at Overlanders! we released 0k for years while maintaining a good amount of fame and love. That's a pretty impressive ratio in my book.
Obviously you have to closely work with the cruncher. Like most, Shrinkler searches for references (i.e. copy-paste from previous location) and entropically encodes the result (length and position, or the byte itself if no reference was found) depending on current contexts.
Once again, there is one context for each address parity. That's why padding might give better results: not only you catch the most fitting context, and if repeating blocks are aligned, the relative position (e.g. -16) might becomes more frequent and so encoded in less bits.
Keep alike sequences of code close, and try to reuse the same opcodes. For instance, always use CP with the same register, always handle LSB first, ...
Macros can help, and so can a style guide.
Store your data so they fit the model. The overhead of the unpacking routine might be swallowed, to speak like Verlaine.
More interesting idea: exploit RSTs. In the following example:
MACRO CRTC reg,val
ld bc,&bc00+reg:out (c),c:ld bc,&bd00+val:out (c),c
The changing bits are interleaved with a common template. We might benefit from:
MACRO CRTC reg,val
RST 0:BYTE reg,val
You can use the same trick to pass parameters to CALLed routines (see next point, though).
As said, addresses are quite arbitrary (unless there are not) thus difficult to crunch. Avoid calls and self-modifications.
final_countdown ld a,0:dec a:ld (final_countdown+1),a ; NO
countdown_to_extinction = &2121
ld hl,countdown_to_extinction:dec (hl) ; YES
Of course, reuse same address for different effects. You may also dedicate IX to handle all variables/counters.