I suggest you to read about the vertex input layout and geometry shaders.
In general: define your goal first, let's say the goal is to use only one draw call and least vertexbuffer updates because you want to keep the gl calls to a minimum. first calculate the uv coordinates for every region in your texture atlas and store them in a hashmap in ram. Then define what parts of your tiles are static and which are dynamic in terms of transformation matrices, this is required to determine the best method of least updates. Using Drawelements is ok for tile rendering.
Let's reformulate your problem: I want to draw a dynamic amount of 2d vertex quads at dynamic positions with dynamic texcoords. That's 3 dynamics, namely the amount of vertex quads, each quads transformation and each quads vertex to texcoord mapping. Maybe each quad should have an dynamic size?
Oldschool solution:
1. Calculate every quad transform on cpu and update all gl buffers every frame. A quad are 6 vertices unindexed or 4 vertices indexed and 2 ushort indices.
2. Use drawelements or if you are even oldschooler use the draw, which means you have to use an indexbuffer and vertexbuffer combination.
3. When your quads should move just update the buffer on cpu, for rendering the whole buffer or portions of it will be updated to vram via glbuffersubdata or glmapbuffer. It means you have a total of 3 - 4 gl calls every frame.
Modern solution.
1. Use uniform buffers to store texcoords and matrices per quad of all tiles if you have lots of dynamic tiles and few static. The uniform buffer is just a struct with two arrays of texcoords and matrices. This way you can manipulate each tiles position and texture without having to modify vbo or vao.
2. Use drawelements and lookup uniformbuffer matrix inside vertexshader.
3. Use a geometry shader to generate your vertex quad inside gpu per matrix texcoord tuple. For simple quads it's faster to be calculated in geometry shader, for geometry like arcs with lots of cos and sin it's slower in geometry shader. In fragment shader just do regular texture mapping.
4. Update uniform buffer and call drawelements. This way you have only 2 gl calls, one to update the uniform buffer and the other to drawelements. On cpu you just update the individual matrices and tecoords inside uniform buffer struct.