Multi-Column index not used for index-only scan, but partial index is
Tuple visibility is the problem.  It uses the bitmap index scan in the unvacuumed case because it thinks it will be faster, not because it is incapable of using the IOS.  Once I do a VACUUM test on your exact example, it starts using the index only scan on index i2, because now it thinks that the IOS will be faster.

As for why it doesn't use the bitmap scan for the partial index when the table is not vacuumed, that is a bit more complicated and is not about visibility. One of the benefits of bitmap scans (and in your case, the only benefit of them) is that it reads the table in physical order and visits each table block only once, regardless of the order of the index. PostgreSQL sees a perfect correlation between id and the order of the physical table rows, so for the index with id as a leading column (the partial index) it think this benefit does not really matter, as it will read the table in physical order anyway even with a regular index scan.  Since bitmap scans have slightly higher CPU cost, it won't do one when it offers no predicted IO advantage.  The correlation between the flag column and the physical table order is lower, and so it thinks bitmap index scans will get a benefit when using the index which has flag as its leading column, which happens to be the full index.  It thinks this benefit outweighs the CPU cost.  If the table was marked all-visible, then the benefits of an index-only-scan would tip the scales back again, but without vacuuming, that doesn't happen.

Now in the case of the index on (flag, id), the leading column is being tested for equality and the only rows selected will have the same value for flag.  So what really matters is the correlation with the next column in the index, id, not with the flag column.  But PostgreSQL isn't (apparently) clever enough to understand this. Now I am kind of curious how hard it would be to make it understand that.