Aside from databases and so on, one efficient choice is a multi-level storage bitmap. Basically, a storage bitmap uses a "1" bit for data, and a "0" bit for no-data. There's a balance depending on how barren your data is, but I think a balanced alternative would have 4-5 levels of bitmaps, resulting in a maximum of 8 MB of bitmaps assuming all cells are filled. The algorithm quickly becomes efficient as the data density drops. The break-even point would be at about 87.5% cell density, the lower your data, the more impressive it can become.
Basically, each chunk is stored on the disk only if it has at least 1 object. This bitmap requires up to 2kb of initial storage data. For each present chunk, break it in to 4x4x4 sub-chunks. This results in a 8 byte bitmap per present chunk.
From there, subdivide those in to 2x2x2 chunks, which requires a 1 byte bitmap each. Finally, one last byte bitmap describes which of the 8 cells are populated in this area. After that, it becomes trivial to just read the object data in sequential order.
If your data density is far less than 50%, one final super-chunk bitmap could cut down the chunk bitmap by adding a small 32 byte super-chunk bitmap for each 4x4x4 super-chunk that contains at least one chunk.
To read the data, simply start from the top bitmap and initialize a counter. For each bit, check if it's a 1, and if so, start processing the next smaller sub-area. After each bit, increment the counter by 1 to keep track of what you're doing. This is basically recursive logic, so some optimization is possible here.
To write the data, initialize a full set of empty maps for each possible level. Then, for each cell, set the appropriate present bits. Finally, output each of the non-zero bitmaps in order from the chunk/super-chunk level down to the cell level.
For performance, you can interleave the objects with the bitmaps; every time you output a 2x2x2 bitmap, output all of the objects in bit order. Other designs are possible too, but this is probably the most straightforward and facilitates sequential reads/writes for optimal saving/loading times.
As you can see, Minecraft itself uses a similar structure, a Region format. A region is a set of chunks stored by proximity, which in turn has a bunch of chunks in it. Note that they use separate files, but for a file the size you're talking about, one region is probably acceptable.