You can still generate meshes per chunk, but to ensure the edges line up, you need to access voxels outside the current chunk. So, your chunks need to know about each other. When accessing a voxel in a chunk, have it access the 'world' level to retrieve the voxel. The 'world' level shouldn't care about which chunk a voxel is in. So inside your chunk it would be something like:
//inside chunk class
getVoxelAt(x, y, z)
this.parentWorld.getVoxelAt(x,y,z)
And the function inside parent world
// inside world class
getVoxelAt(x,y,z)
chunk = getChunkThatContains(x,y,z)
chunk.getChunkLocalVoxel(x - chunk.base.x, y - chunk.base.y, z - chunk.base.z)
Where getChunkLocalVoxel
takes the local coordinates of the voxel. So it would access whatever data structure you're using to hold chunk data, like an array:
//inside chunk class
getChunkLocalVoxel(x,y,z)
return voxels[x][y][z];
You're likely getting gaps now because when you come to the edge of a chunk, you assume there's nothing beyond the edge when generating your mesh. So the mesh has gaps.