So you want the same effect as setting the wrap mode to GL_REPEAT, but for a tile rather than the entire texture, right?
You need to pass in the size of the tile and its position within the atlas, both as fractions of the texture’s size (i.e. in the range [0,1]). E.g.
uniform vec2 position;
uniform vec2 size;
...
texcoord = position + size * frac(texcoord);
If you’re using mipmaps, there’s the added complication that you can’t use the implicit derivatives generated by texture(), as those will be incorrect when you wrap around the edge of a tile. Instead, you need to calculate derivatives from the original texture coordinates and use textureGrad(), e.g.:
uniform vec2 position;
uniform vec2 size;
...
vec2 dx = dFdx(texcoord);
vec2 dy = dFdy(texcoord);
texcoord = position + size * frac(texcoord);
vec4 color = textureGrad(tex, texcoord, dx, dy);
A 2D texture array is similar to a 3D texture in that it has a third dimension. But unlike a 3D texture, mipmaps are only in 2 dimensions (so for e.g. a 64x64x16 2D texture array, the next mipmap level would be 32x32x16, not 32x32x8), filtering is only performed in 2D dimensions, and the third texture coordinate is unnormalised (it’s just rounded to the nearest integer to select the layer). 2D texture arrays are created using glTexImage3D with the target GL_TEXTURE_2D_ARRAY and accessed via a uniform of type sampler2DArray in the shader.
If you’re using a 2D texture array, each tile would be the full width and height of the texture so you can just use GL_REPEAT. Texture arrays require OpenGL 3.0 or the EXT_texture_array extension.