Nice introduction, thank you for sharing!
1) I know that I do multiply before I add, but I always struggle if I do AND before OR (right?). Maybe you could add a statement about the priority of the logic operations. E.g. NOT is done first, then ...
2) You switch to binary representation to determine which address bits are actually relevant, which is ok. But then you switch back to discuss "address windows". That works ok for your examples, where you basically have only one I/O windows.
I rather like to break down the addresses on the binary level, starting from the higher address bits down to the lower ones. So for example you first ROM area becomes
ROM1=(A15 & !A14)
# (A15 & A14 & !A13 & !A12)
ROM2=(A15 & A14 & !A13 & A12 & A11)
# (A15 & A14 & A13)
where in the first term the first part is the 16k area from $8000 to $BFFF, and the second part is $C000-$CFFF. The second term (for ROM2) then has two parts, for $D800-$DFFF and $E000-$FFFF.
Breaking down the addresses like that "reduces" the terms to normal form which allows you to do optimizations in your address decoding. If you would use for example two different ICs or other memory-mapped devices (so you would not combine ROM1 and ROM2 as you did in your example), you could for example reduce the number of terms by moving the area at $D800 to something closer to a say a 16k boarder.
Ok, thinking about it now this is probably more like "old school", where the address decoding is done in discrete logic, and not in programmable logic, and you more needed to optimize e.g. due to timing requirements The resulting decoding scheme is the same anyway and the "normal" form is less grokable than your address window form.
Probably you could just emphasize the switch between the different representations.