The critical flaw in your format is treating the decimal field as a normal integer. Rather than the bits being 2^31:2^30:...:2^1:2^0, they should be 2^(-1):2^(-2):...:2^(-31).
So for 1.5 (for example), you'd have integer = 1 and decimal = 0x8000_0000 (i.e., 0b1000_0000_0000_0000). Then 1.5 + 1.5 = 2 + (0x1_0000_0000) which is an overflow in the decimal field thus yielding 3.0.
Do that and your code will clean up dramatically (while adding some complexity to your string conversions).
Edit: I should also point out that you don't need to use two fields for this. You could simply use a 64-bit integer. Where you choose to put your decimal point is arbitrary since it only matters when you're doing conversions between your fixed format and a floating point format (either with floats or with string representations) - the only caveat being that you need a way to keep track of where you decided the decimal point is for conversions. So you could, for a simple example, use an unsigned 8-bit and decide for your application that you only need 4 bits of decimal precision. That would give you a range of 0.0 to 15.9375 (0b1111_1111 = 15 + (1/2 + 1/4 + 1/8 + 16). If you decided that you only need 1 bit of decimal precision, the same 8-bit would give you a range of 0.0 to 127.5 (0b1111111_1 = 127 + 1/2). Signed integers also work, I was just too lazy to lay it out for you.
All of the normal integer mathematical operations work on this with no special handling. It's only your idea of what those bits mean that matters and then only when you're doing conversions. So you could do ::from(1.5) or ::from("1.5") and fill your fixed-point integer accordingly. You'd just need to instantiate your fixed point field with the number of decimal precision points (or just assume it's always static for your field - so for your example, you'd just use a 64-bit signed integer and always have 32 bits of integer and 32 bits of decimal).