If you only need per-face normals, and if your texcoords for a face are strictly 0/0, 0/1, 1/0, 1/1 (or similar to suit your layout) then you can construct a cube with 8 verts and either 30 (strip with restart) or 36 (list) indexes. Fetch the normals and texcoords using a constant array lookup based on SV_VertexID in your vertex shader.
Doing this means that you don't even need to include texcoords or normals in your vertex buffer, which will give you even more of a memory saving.
Going further, you could still go all the way to 24 verts per cube but also use instancing. Each cube would be a fixed-size in your vertex buffer (1x1x1) and you'd have a scaling factor and position (assuming your cubes don't rotate, a matrix if they do) as per-instance data. In the non-rotating case you've a once-off cost of 24 verts, but then each cube just needs 6 floats to fully specify. In the rotating case you're looking at 16 floats, but even that is a substantial saving (you're more likely to bottleneck CPU-side on matrix transforms in this case - for the non-rotating case constructing a matrix on the fly in your vertex shader - even if it's done per-vertex, is so stupidly fast that you don't even need to worry about it).
For per-face textures, just use a texture array. You need to ensure that each such texture in the array is the same size, of course, and you'll still need to break your current batch if the array itself needs to change, but otherwise it'll do the job just fine. Add a third texcoord to your vertex definition which defines the array slice to use for each face.
You don't need a GS with this, and it should run faster than using one as having the geometry shader stage enabled will impose extra overhead of it's own.
I've benchmark code in my engine that just draws a bunch of cubes using this method, and I can easily chew through over 300,000 cubes while still clearing 60fps, on a relatively low-end GPU, and without doing anything else to optimize the process. Admittedly I'm neither lighting nor texturing them, but I do have alpha blending enabled, backface culling disabled, and overall it balances against my "not doing anything else to optimize" part, so it should give you a reasonable idea of the kind of ballpark you can hit with this method.